From afa1273e23fdb968da84651450da0cfed5b2751d Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Fri, 3 May 2019 15:11:08 -0700 Subject: [PATCH] lokinet monitor --- contrib/node-monitor/ini.js | 62 ++ contrib/node-monitor/lokinet.js | 1196 ++++++++++++++++++++++++ contrib/node-monitor/monitor_client.js | 322 +++++++ contrib/node-monitor/package-lock.json | 29 + contrib/node-monitor/package.json | 25 + 5 files changed, 1634 insertions(+) create mode 100644 contrib/node-monitor/ini.js create mode 100644 contrib/node-monitor/lokinet.js create mode 100644 contrib/node-monitor/monitor_client.js create mode 100644 contrib/node-monitor/package-lock.json create mode 100644 contrib/node-monitor/package.json diff --git a/contrib/node-monitor/ini.js b/contrib/node-monitor/ini.js new file mode 100644 index 000000000..1ab137d4c --- /dev/null +++ b/contrib/node-monitor/ini.js @@ -0,0 +1,62 @@ +function iniToJSON(data) { + const lines = data.split(/\n/) + var section = 'unknown' + var config = {} + for(var i in lines) { + var line = lines[i].trim() + if (line.match(/#/)) { + var parts = line.split(/#/) + line = parts[0].trim() + } + // done reducing + if (!line) continue + + // check for section + if (line[0] == '[' && line[line.length - 1] == ']') { + section = line.substring(1, line.length -1) + //console.log('found section', section) + continue + } + // key value pair + if (line.match(/=/)) { + var parts = line.split(/=/) + var key = parts.shift().trim() + var value = parts.join('=').trim() + if (value === 'true') value = true + if (value === 'false') value = false + //console.log('key/pair ['+section+']', key, '=', value) + if (config[section] === undefined) config[section] = {} + config[section][key]=value + continue + } + console.error('config ['+section+'] not section or key/value pair', line) + } + return config +} + +function jsonToINI(json) { + var lastSection = 'unknown' + var config = '' + for(var section in json) { + config += "\n" + '[' + section + ']' + "\n" + var keys = json[section] + for(var key in keys) { + //console.log('key', key, 'value', keys[key]) + // if keys[key] is an array, then we need to send the same key each time + if (keys[key] !== undefined && keys[key].constructor.name == 'Array') { + for(var i in keys[key]) { + var v = keys[key][i] + config += key + '=' + v + "\n" + } + } else { + config += key + '=' + keys[key] + "\n" + } + } + } + return config +} + +module.exports = { + iniToJSON: iniToJSON, + jsonToINI: jsonToINI, +} diff --git a/contrib/node-monitor/lokinet.js b/contrib/node-monitor/lokinet.js new file mode 100644 index 000000000..d9e65e6cc --- /dev/null +++ b/contrib/node-monitor/lokinet.js @@ -0,0 +1,1196 @@ +// no npm! +const os = require('os') +const fs = require('fs') +const dns = require('dns') +const net = require('net') +const ini = require('./ini') +const path = require('path') +const http = require('http') +const https = require('https') +const { spawn, exec } = require('child_process') + +// FIXME: disable rpc if desired +const VERSION = 0.6 +console.log('lokinet launcher version', VERSION, 'registered') + +function log() { + var args = [] + for(var i in arguments) { + var arg = arguments[i] + //console.log('arg type', arg, 'is', typeof(arg)) + if (typeof(arg) == 'object') { + arg = JSON.stringify(arg) + } + args.push(arg) + } + console.log('NETWORK:', args.join(' ')) +} + +function getBoundIPv4s() { + var nics = os.networkInterfaces() + var ipv4s = [] + for(var adapter in nics) { + var ips = nics[adapter] + for(var ipIdx in ips) { + var ipMap = ips[ipIdx] + if (ipMap.address.match(/\./)) { + ipv4s.push(ipMap.address) + } + } + } + return ipv4s +} + +var auto_config_test_port, auto_config_test_host, auto_config_test_ips +// this doesn't need to connect completely to get our ip +// it won't get our IP if DNS doesn't work +function getNetworkIP(callback) { + // randomly select an ip + //log('getNetworkIP', auto_config_test_ips) + var ip = auto_config_test_ips[Math.floor(Math.random() * auto_config_test_ips.length)] + //log('getNetworkIP from', ip, auto_config_test_port) + var socket = net.createConnection(auto_config_test_port, ip) + socket.setTimeout(5000) + socket.on('connect', function() { + callback(undefined, socket.address().address) + socket.end() + }) + var abort = false + socket.on('timeout', function() { + if (socket.address().address) { + abort = true + var resultIp = socket.address().address + log('getNetworkIP timeout but still got outgoing IP:', resultIp) + socket.destroy() + callback(undefined, resultIp) + } else { + // don't have what we need, just wait it out, maybe we'll get lucky + //log('getNetworkIP timeout') + //callback('timeout', 'error') + } + }) + socket.on('error', function(e) { + console.error('NETWORK: getNetworkIP error', e) + // FIXME: maybe a retry here + log('getNetworkIP failure test', socket.address().address) + log('getNetworkIP failure, retry?') + if (abort) { + log('getNetworkIP already timed out') + return + } + callback(e, 'error') + }) +} + +function getIfNameFromIP(ip) { + var nics = os.networkInterfaces() + var ipv4s = [] + for(var adapter in nics) { + var ips = nics[adapter] + for(var ipIdx in ips) { + var ipMap = ips[ipIdx] + if (ipMap.address == ip) { + return adapter + } + } + } + return '' +} + +const urlparser = require('url') + +function httpGet(url, cb) { + const urlDetails = urlparser.parse(url) + //console.log('httpGet url', urlDetails) + //console.log('httpGet', url) + //console.trace('who started dis', url) + var protoClient = http + if (urlDetails.protocol == 'https:') { + protoClient = https + } + // well somehow this can get hung on macos + var abort = false + var watchdog = setInterval(function() { + if (shuttingDown) { + //if (cb) cb() + // [', url, '] + log('hung httpGet but have shutdown request, calling back early and setting abort flag') + clearInterval(watchdog) + abort = true + cb() + return + } + }, 5000) + protoClient.get({ + hostname: urlDetails.hostname, + protocol: urlDetails.protocol, + port: urlDetails.port, + path: urlDetails.path, + timeout: 5000, + }, (resp) => { + //log('httpGet setting up handlers') + clearInterval(watchdog) + resp.setEncoding('binary') + let data = '' + // A chunk of data has been recieved. + resp.on('data', (chunk) => { + data += chunk + }) + // The whole response has been received. Print out the result. + resp.on('end', () => { + log('result code', resp.statusCode) + if (abort) { + // we already called back + return + } + if (resp.statusCode == 404) { + console.error('NETWORK:', url, 'is not found') + cb() + return + } + cb(data) + }) + }).on("error", (err) => { + console.error("NETWORK: httpGet Error: " + err.message, 'port', urlDetails.port) + //console.log('err', err) + cb() + }) +} + +function dynDNSHandler(data, cb) { + +} + +function getPublicIPv6(cb) { +//v6.ident.me +} + +var getPublicIPv4_retries = 0 +function getPublicIPv4(cb) { + // trust more than one source + // randomly find 2 matching sources + + // dns is faster than http + // dig +short myip.opendns.com @resolver1.opendns.com + // httpGet doesn't support https yet... + var publicIpServices = [ + { url: 'https://api.ipify.org' }, + { url: 'https://ipinfo.io/ip' }, + { url: 'https://ipecho.net/plain' }, + //{ url: 'http://api.ipify.org' }, + //{ url: 'http://ipinfo.io/ip' }, + //{ url: 'http://ipecho.net/plain' }, + { url: 'http://ifconfig.me' }, + { url: 'http://ipv4.icanhazip.com' }, + { url: 'http://v4.ident.me' }, + { url: 'http://checkip.amazonaws.com' }, + //{ url: 'https://checkip.dyndns.org', handler: dynDNSHandler }, + ] + var service = [] + service[0] = Math.floor(Math.random() * publicIpServices.length) + service[1] = Math.floor(Math.random() * publicIpServices.length) + var done = [ false, false ] + function markDone(idx, value) { + if (value === undefined) value = '' + done[idx] = value.trim() + let ready = true + //log('done', done) + for(var i in done) { + if (done[i] === false) { + ready = false + log('getPublicIPv4', i, 'is not ready') + break + } + } + if (!ready) return + log('getPublicIPv4 look ups are done', done) + if (done[0] != done[1]) { + // try 2 random services again + getPublicIPv4_retries++ + if (getPublicIPv4_retries > 10) { + console.error('NAT detection: Can\'t determine public IP address') + process.exit() + } + getPublicIPv4(cb) + } else { + // return + //log("found public IP", done[0]) + cb(done[0]) + } + } + + function doCall(number) { + httpGet(publicIpServices[service[number]].url, function(ip) { + if (ip === false) { + service[number] = (Math.random() * publicIpServices.length) + // retry + console.warn(publicIpServices[service[number]].url, 'failed, retrying') + doCall(number) + return + } + console.log(number, publicIpServices[service[number]].url, ip) + markDone(number, ip) + }) + } + doCall(0) + doCall(1) +} + +// used for generating temp filenames +function randomString(len) { + var text = "" + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + for (var i = 0; i < len; i++) + text += possible.charAt(Math.floor(Math.random() * possible.length)) + return text +} + +function isDnsPort(ip, port, cb) { + const resolver = new dns.Resolver() + resolver.setServers([ip + ':' + port]) + resolver.resolve(auto_config_test_host, function(err, records) { + if (err) console.error('resolve error:', err) + log(auto_config_test_host, records) + cb(records !== undefined) + }) +} + +function testDNSForLokinet(server, cb) { + const resolver = new dns.Resolver() + resolver.setServers([server]) + // incase server is 127.0.0.1:undefined + try { + resolver.resolve('localhost.loki', function(err, records) { + if (err) console.error('NETWORK: localhost.loki resolve err:', err) + //log(server, 'localhost.loki test results', records) + cb(records) + }) + } catch(e) { + console.error('NETWORK: testDNSForLokinet error, incorrect server?', server) + cb() + } +} + +function lookup(host, cb) { + var resolver = new dns.Resolver() + //console.log('lokinet lookup servers', runningConfig.dns.bind) + resolver.setServers([runningConfig.dns.bind]) + resolver.resolve(host, function(err, records) { + if (err) { + // not as bad... that's at least a properly formatted response + if (err.code == 'ENOTFOUND') { + records = null + } else + // leave bad + if (err.code == 'ETIMEOUT') { + records = undefined + } else { + console.error('lokinet lookup unknown err', err) + } + } + //console.log(host, 'lokinet dns test results', records) + cb(records) + }) +} + +function findLokiNetDNS(cb) { + const localIPs = getBoundIPv4s() + var checksLeft = 0 + var servers = [] + function checkDone() { + if (shuttingDown) { + //if (cb) cb() + log('not going to start lokinet, shutting down') + return + } + checksLeft-- + if (checksLeft<=0) { + log('readResolv done') + cb(servers) + } + } + /* + var resolvers = dns.getServers() + console.log('Current resolvers', resolvers) + // check local DNS servers in resolv config + for(var i in resolvers) { + const server = resolvers[i] + var idx = localIPs.indexOf(server) + if (idx != -1) { + // local DNS server + console.log('local DNS server detected', server) + checksLeft++ + testDNSForLokinet(server, function(isLokinet) { + if (isLokinet) { + // lokinet + console.log(server, 'is a lokinet DNS server') + servers.push(server) + } + checkDone() + }) + } + } + */ + // maybe check all local ips too + for(var i in localIPs) { + const server = localIPs[i] + checksLeft++ + testDNSForLokinet(server, function(isLokinet) { + if (isLokinet !== undefined) { + // lokinet + log(server, 'is a lokinet DNS server') + servers.push(server) + } + checkDone() + }) + } +} + +function readResolv(dns_ip, cb) { + const localIPs = getBoundIPv4s() + var servers = [] + var checksLeft = 0 + //log('make sure we exclude?', dns_ip) + + function checkDone() { + if (shuttingDown) { + //if (cb) cb() + log('not going to start lokinet, shutting down') + return + } + checksLeft-- + //log('readResolv reports left', checksLeft) + if (checksLeft<=0) { + log('readResolv done') + cb(servers) + } + } + + var resolvers = dns.getServers() + log('Current resolvers', resolvers) + for(var i in resolvers) { + const server = resolvers[i] + if (server == dns_ip) { + log('preventing DNS loop on', dns_ip) + continue + } + var idx = localIPs.indexOf(server) + if (idx != -1) { + log('local DNS server detected', server) + checksLeft++ // wait for it + testDNSForLokinet(server, function(isLokinet) { + if (isLokinet === undefined) { + // not lokinet + log(server, 'is not a lokinet DNS server') + servers.push(server) + } + checkDone() + }) + } else { + // non-local + log('found remote DNS server', server) + servers.push(server) + } + } + checksLeft++ + checkDone() + /* + const data = fs.readFileSync('/etc/resolv.conf', 'utf-8') + const lines = data.split(/\n/) + + for(var i in lines) { + var line = lines[i].trim() + if (line.match(/#/)) { + var parts = line.split(/#/) + line = parts[0].trim() + } + // done reducing + if (!line) continue + if (line.match(/^nameserver /)) { + const server = line.replace(/^nameserver /, '') + var idx = localIPs.indexOf(server) + if (idx != -1) { + console.log('local DNS server detected', server) + const resolver = new dns.Resolver() + resolver.setServers([server]) + checksLeft++ + resolver.resolve('localhost.loki', function(err, records) { + //if (err) console.error(err) + //console.log('local dns test results', records) + if (records === undefined) { + // not lokinet + console.log(server, 'is not a lokinet DNS server') + servers.push(server) + } + checkDone() + }) + } else { + // non-local + console.log('found remote DNS server', server) + servers.push(server) + } + continue + } + checkDone() + console.error('readResolv unknown', line) + } + return servers + */ +} + +// this can really delay the start of lokinet +function findFreePort53(ips, index, cb) { + log('testing', ips[index], 'port 53') + isDnsPort(ips[index], 53, function(res) { + //console.log('isDnsPort res', res) + // false + if (!res) { + log('Found free port 53 on', ips[index], index) + cb(ips[index]) + return + } + log('Port 53 is not free on', ips[index], index) + if (index + 1 == ips.length) { + cb() + return + } + findFreePort53(ips, index + 1, cb) + }) +} + +// https://stackoverflow.com/a/40686853 +function mkDirByPathSync(targetDir, { isRelativeToScript = false } = {}) { + const sep = path.sep + const initDir = path.isAbsolute(targetDir) ? sep : '' + const baseDir = isRelativeToScript ? __dirname : '.' + + return targetDir.split(sep).reduce((parentDir, childDir) => { + const curDir = path.resolve(baseDir, parentDir, childDir) + try { + fs.mkdirSync(curDir) + } catch (err) { + if (err.code === 'EEXIST') { // curDir already exists! + return curDir + } + + // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows. + if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure. + throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`) + } + + const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1 + if (!caughtErr || caughtErr && curDir === path.resolve(targetDir)) { + throw err // Throw if it's just the last created dir. + } + } + + return curDir + }, initDir) +} + +function makeMultiplatformPath(path) { + return path +} + +var cleanUpBootstrap = false +var cleanUpIni = false +function generateINI(config, need, markDone, cb) { + const homeDir = os.homedir() + //console.log('homeDir', homeDir) + //const data = fs.readFileSync(homeDir + '/.lokinet/lokinet.ini', 'utf-8') + //const jConfig = iniToJSON(data) + //console.dir(jConfig) + //const iConfig = jsonToINI(jConfig) + //console.log(iConfig) + var upstreams, lokinet_free53Ip, lokinet_nic + var use_lokinet_rpc_port = config.rpc_port + var lokinet_bootstrap_path = homeDir + '/.lokinet/bootstrap.signed' + var lokinet_nodedb = homeDir + '/.lokinet/netdb' + if (config.netid) { + lokinet_nodedb += '-' + config.netid + } + if (!fs.existsSync(lokinet_nodedb)) { + log('making', lokinet_nodedb) + mkDirByPathSync(lokinet_nodedb) + } + var upstreamDNS_servers = [] + var params = { + upstreamDNS_servers: upstreamDNS_servers, + lokinet_free53Ip: lokinet_free53Ip, + lokinet_nodedb: lokinet_nodedb, + lokinet_bootstrap_path: lokinet_bootstrap_path, + lokinet_nic: lokinet_nic, + use_lokinet_rpc_port: use_lokinet_rpc_port, + } + if (config.bootstrap_url) { + httpGet(config.bootstrap_url, function(bootstrapData) { + if (bootstrapData) { + cleanUpBootstrap = true + const tmpRcPath = os.tmpdir() + '/' + randomString(8) + '.lokinet_signed' + fs.writeFileSync(tmpRcPath, bootstrapData, 'binary') + log('boostrap wrote', bootstrapData.length, 'bytes to', tmpRcPath) + //lokinet_bootstrap_path = tmpRcPath + params.lokinet_bootstrap_path = tmpRcPath + config.bootstrap_path = tmpRcPath + } + markDone('bootstrap', params) + }) + } else { + // seed version + //params.lokinet_bootstrap_path = '' + markDone('bootstrap', params) + } + readResolv(config.dns_ip, function(servers) { + upstreamDNS_servers = servers + params.upstreamDNS_servers = servers + upstreams = 'upstream='+servers.join('\nupstream=') + markDone('upstream', params) + }) + log('trying', 'http://'+config.rpc_ip+':'+config.rpc_port) + httpGet('http://'+config.rpc_ip+':'+config.rpc_port, function(testData) { + //log('rpc has', testData) + if (testData !== undefined) { + log('Bumping RPC port', testData) + // FIXME: retest new port + use_lokinet_rpc_port = use_lokinet_rpc_port + 1 + params.use_lokinet_rpc_port = use_lokinet_rpc_port + } + markDone('rpcCheck', params) + }) + var skipDNS = false + if (config.dns_ip || config.dns_port) { + skipDNS = true + markDone('dnsBind', params) + } + getNetworkIP(function(e, ip) { + if (ip == 'error' || !ip) { + console.error('NETWORK: can\'t detect default adapter IP address') + // can't handle the exits here because we don't know if it's an actual requirements + // if we need netIf or dnsBind + if (done.netIf !== undefined || done.dnsBind !== undefined) { + process.exit() + } + } + log('detected outgoing interface ip', ip) + lokinet_nic = getIfNameFromIP(ip) + params.lokinet_nic = lokinet_nic + params.interfaceIP = ip + log('detected outgoing interface', lokinet_nic) + markDone('netIf', params) + if (skipDNS) return + var tryIps = ['127.0.0.1'] + if (os.platform() == 'linux') { + tryIps.push('127.3.2.1') + } + tryIps.push(ip) + findFreePort53(tryIps, 0, function(free53Ip) { + if (free53Ip === undefined) { + console.error('NETWORK: Cant automatically find an IP to put a lokinet DNS server on') + // can't handle the exits here because we don't know if it's an actual requirements + if (done.dnsBind !== undefined) { + process.exit() + } + } + lokinet_free53Ip = free53Ip + params.lokinet_free53Ip = free53Ip + log('binding DNS port 53 to', free53Ip) + markDone('dnsBind', params) + }) + }) + getPublicIPv4(function(ip) { + //log('generateINI - ip', ip) + params.publicIP = ip + markDone('publicIP', params) + }) +} + +// unified post auto-config adjustments +// disk to running +function applyConfig(file_config, config_obj) { + // bootstrap section + // router mode: bootstrap is optional (might be a seed if not) + // client mode: bootstrap is required, can't have a seed client + if (file_config.bootstrap_path) { + config_obj.bootstrap = { + 'add-node': file_config.bootstrap_path + } + } + // router section + if (file_config.nickname) { + config_obj.router.nickname = file_config.nickname + } + // set default netid based on testnet + if (file_config.lokid && file_config.lokid.network == "test") { + config_obj.router.netid = 'service' + //runningConfig.network['ifaddr'] = '10.254.0.1/24' // hack for Ryan's box + } + if (file_config.netid) { + config_obj.router.netid = file_config.netid + } + // network section + if (file_config.ifname) { + config_obj.network.ifname = file_config.ifname + } + if (file_config.ifaddr) { + config_obj.network.ifaddr = file_config.ifaddr + } + // dns section + if (file_config.dns_ip || file_config.dns_port) { + // FIXME: dynamic dns ip + // we'd have to move the DNS autodetection here + // detect free port 53 on ip + // for now just make sure we have sane defaults + var ip = file_config.dns_ip + if (!ip) ip = '127.0.0.1' + var dnsPort = file_config.dns_port + if (dnsPort === undefined) dnsPort = 53 + config_obj.dns.bind = ip + ':' + dnsPort + } +} + +var runningConfig = {} +var genSnCallbackFired +function generateSerivceNodeINI(config, cb) { + const homeDir = os.homedir() + var done = { + bootstrap: false, + upstream : false, + rpcCheck : false, + dnsBind : false, + netIf : false, + publicIP : false, + } + if (config.publicIP) { + done.publicIP = undefined + } + genSnCallbackFired = false + function markDone(completeProcess, params) { + done[completeProcess] = true + let ready = true + for(var i in done) { + if (!done[i]) { + ready = false + log(i, 'is not ready') + break + } + } + if (shuttingDown) { + //if (cb) cb() + log('not going to start lokinet, shutting down') + return + } + if (!ready) return + // we may have un-required proceses call markDone after we started + if (genSnCallbackFired) return + genSnCallbackFired = true + var keyPath = homeDir + '/.loki/' + // + if (config.lokid.data_dir) { + keyPath = config.lokid.data_dir + // make sure it has a trailing slash + if (keyPath[keyPath.length - 1]!='/') { + keyPath += '/' + } + } + if (config.lokid.network == "test" || config.lokid.network == "demo") { + keyPath += 'testnet/' + } + keyPath += 'key' + log('markDone params', JSON.stringify(params)) + log('PUBLIC', params.publicIP, 'IFACE', params.interfaceIP) + var useNAT = false + if (params.publicIP != params.interfaceIP) { + log('NAT DETECTED MAKE SURE YOU FORWARD UDP PORT', config.public_port, 'on', params.publicIP, 'to', params.interfaceIP) + useNAT = true + } + log('Drafting lokinet service node config') + // FIXME: lock down identity.private for storage server + runningConfig = { + router: { + nickname: 'ldl', + }, + dns: { + upstream: params.upstreamDNS_servers, + bind: params.lokinet_free53Ip + ':53', + }, + netdb: { + dir: params.lokinet_nodedb, + }, + bind: { + // will be set after + }, + network: { + }, + api: { + enabled: true, + bind: config.rpc_ip + ':' + params.use_lokinet_rpc_port + }, + lokid: { + enabled: true, + jsonrpc: config.lokid.rpc_ip + ':' + config.lokid.rpc_port, + username: config.lokid.rpc_user, + password: config.lokid.rpc_pass, + 'service-node-seed': keyPath + } + } + if (useNAT) { + runningConfig.router['public-ip'] = params.publicIP + runningConfig.router['public-port'] = config.public_port + } + // inject manual NAT config? + if (config.public_ip) { + runningConfig.router['public-ip'] = config.public_ip + runningConfig.router['public-port'] = config.public_port + } + runningConfig.bind[params.lokinet_nic] = config.public_port + if (config.internal_port) { + runningConfig.bind[params.lokinet_nic] = config.internal_port + } + applyConfig(config, runningConfig) + // optional bootstrap (might be a seed if not) + // doesn't work + //runningConfig.network['type'] = 'null' // disable exit + //runningConfig.network['enabled'] = true; + cb(ini.jsonToINI(runningConfig)) + } + generateINI(config, done, markDone, cb) +} + +var genClientCallbackFired +function generateClientINI(config, cb) { + var done = { + bootstrap: false, + upstream : false, + rpcCheck : false, + dnsBind : false, + } + genClientCallbackFired = false + function markDone(completeProcess, params) { + done[completeProcess] = true + let ready = true + for(var i in done) { + if (!done[i]) { + ready = false + log(i, 'is not ready') + break + } + } + if (!ready) return + // make sure we didn't already start + if (genClientCallbackFired) return + genClientCallbackFired = true + if (!params.use_lokinet_rpc_port) { + // use default because we enable it + params.use_lokinet_rpc_port = 1190 + } + log('Drafting lokinet client config') + runningConfig = { + router: { + nickname: 'ldl', + }, + dns: { + upstream: params.upstreamDNS_servers, + bind: params.lokinet_free53Ip + ':53', + }, + netdb: { + dir: params.lokinet_nodedb, + }, + network: { + }, + api: { + enabled: true, + bind: config.rpc_ip + ':' + params.use_lokinet_rpc_port + }, + } + applyConfig(config, runningConfig) + // a bootstrap is required, can't have a seed client + if (!runningConfig.bootstrap) { + console.error('no bootstrap for client') + process.exit() + } + cb(ini.jsonToINI(runningConfig)) + } + generateINI(config, done, markDone, cb) +} + +var shuttingDown +var lokinet +var lokinetLogging = true +function preLaunchLokinet(config, cb) { + //console.log('userInfo', os.userInfo('utf8')) + //console.log('started as', process.getuid(), process.geteuid()) + + // check user permissions + if (os.platform() == 'darwin') { + if (process.getuid() != 0) { + console.error('MacOS requires you start this with sudo') + process.exit() + } + // leave the linux commentary for later + /* + } else { + if (process.getuid() == 0) { + console.error('Its not recommended you run this as root') + } */ + } + + if (os.platform() == 'linux') { + // not root-like + exec('getcap ' + config.binary_path, function (error, stdout, stderr) { + //console.log('stdout', stdout) + // src/loki-network/lokinet = cap_net_bind_service,cap_net_admin+eip + if (!(stdout.match(/cap_net_bind_service/) && stdout.match(/cap_net_admin/))) { + if (process.getgid() != 0) { + conole.log(config.binary_path, 'does not have setcap. Please setcap the binary (make install usually does this) or run launcher root one time, so we can') + process.exit() + } else { + // are root + log('going to try to setcap your binary, so you dont need root') + exec('setcap cap_net_admin,cap_net_bind_service=+eip ' + config.binary_path, function (error, stdout, stderr) { + log('binary permissions upgraded') + }) + } + } + }) + } + + // lokinet will crash if this file is zero bytes + if (fs.existsSync('profiles.dat')) { + var stats = fs.statSync('profiles.dat') + if (!stats.size) { + log('cleaning router profiles') + fs.unlinkSync('profiles.dat') + } + } + + const tmpDir = os.tmpdir() + const tmpPath = tmpDir + '/' + randomString(8) + '.lokinet_ini' + cleanUpIni = true + config.ini_writer(config, function (iniData) { + if (shuttingDown) { + //if (cb) cb() + log('not going to write lokinet config, shutting down') + return + } + log(iniData, 'as', tmpPath) + fs.writeFileSync(tmpPath, iniData) + config.ini_file = tmpPath + cb() + }) +} + +function launchLokinet(config, cb) { + if (shuttingDown) { + //if (cb) cb() + log('not going to start lokinet, shutting down') + return + } + if (!fs.existsSync(config.ini_file)) { + log('lokinet config file', config.ini_file, 'does not exist') + process.exit() + } + // command line options + var cli_options = [config.ini_file] + if (config.verbose) { + cli_options.push('-v') + } + console.log('network: launching', config.binary_path, cli_options.join(' ')) + lokinet = spawn(config.binary_path, cli_options) + + if (!lokinet) { + console.error('failed to start lokinet, exiting...') + // proper shutdown? + process.exit() + } + lokinet.killed = false + lokinet.stdout.on('data', (data) => { + var parts = data.toString().split(/\n/) + parts.pop() + data = parts.join('\n') + if (module.exports.onMessage) { + module.exports.onMessage(data) + } + }) + + lokinet.stderr.on('data', (data) => { + if (module.exports.onError) { + module.exports.onError(data) + } + }) + + lokinet.on('close', (code) => { + log(`lokinet process exited with code ${code}`) + // code 0 means clean shutdown + lokinet.killed = true + // clean up + // if we have a temp bootstrap, clean it + if (cleanUpBootstrap && runningConfig.bootstrap['add-node'] && fs.existsSync(runningConfig.bootstrap['add-node'])) { + fs.unlinkSync(runningConfig.bootstrap['add-node']) + } + if (cleanUpIni && fs.existsSync(config.ini_file)) { + fs.unlinkSync(config.ini_file) + } + if (!shuttingDown) { + if (config.restart) { + // restart it in 30 seconds to avoid pegging the cpu + setTimeout(function() { + log('loki_daemon is still running, restarting lokinet') + launchLokinet(config) + }, 30 * 1000) + } else { + // don't restart... + } + } + // else we're shutting down + }) + if (cb) cb() +} + +function checkConfig(config) { + if (config === undefined) config = {} + + if (config.auto_config_test_ips === undefined) config.auto_config_test_ips = ['1.1.1.1', '8.8.8.8'] + if (config.auto_config_test_host === undefined ) config.auto_config_test_host='www.imdb.com' + if (config.auto_config_test_port === undefined ) config.auto_config_test_port=80 + auto_config_test_port = config.auto_config_test_port + auto_config_test_host = config.auto_config_test_host + auto_config_test_ips = config.auto_config_test_ips + + if (config.binary_path === undefined ) config.binary_path='/usr/local/bin/lokinet' + + // we don't always want a bootstrap (seed mode) + + // maybe if no port we shouldn't configure it + if (config.rpc_ip === undefined ) config.rpc_ip='127.0.0.1' + if (config.rpc_port === undefined ) config.rpc_port=0 + + // set public_port ? +} + +function waitForUrl(url, cb) { + httpGet(url, function(data) { + //console.log('rpc data', data) + // will be undefined if down (ECONNREFUSED) + // if success + // Unauthorized Access

401 Unauthorized

+ if (data) { + cb() + } else { + // no data could me 404 + if (shuttingDown) { + //if (cb) cb() + log('not going to start lokinet, shutting down') + return + } + setTimeout(function() { + waitForUrl(url, cb) + }, 1000) + } + }) +} + +function startServiceNode(config, cb) { + // FIXME: if no bootstrap stomp it + // but allow for seed nodes (no bootstrap)? + checkConfig(config) + config.ini_writer = generateSerivceNodeINI + config.restart = true + // FIXME: check for bootstrap stomp and strip it + // only us lokinet devs will need to make our own seed node + preLaunchLokinet(config, function() { + // test lokid rpc port first + // also this makes sure the service key file exists + var url = 'http://'+config.lokid.rpc_user+':'+config.lokid.rpc_pass+'@'+config.lokid.rpc_ip+':'+config.lokid.rpc_port + log('lokinet waiting for lokid RPC server') + waitForUrl(url, function() { + launchLokinet(config, cb) + }) + }) +} + +function startClient(config, cb) { + checkConfig(config) + if (config.bootstrap_path === undefined && config.bootstrap_url === undefined ) config.bootstrap_url='https://i2p.rocks/bootstrap.signed' + config.ini_writer = generateClientINI + preLaunchLokinet(config, function() { + launchLokinet(config, cb) + }) +} + +// return a truish value if so +function isRunning() { + // should we block until port is responding? + return lokinet +} + +// copied from lib +function isPidRunning(pid) { + if (pid === undefined) { + console.trace('isPidRunning was passed undefined, reporting not running') + return false + } + try { + process.kill(pid, 0) + //console.log('able to kill', pid) + return true + } catch(e) { + if (e.code == 'ERR_INVALID_ARG_TYPE') { + // means pid was undefined + return true + } + if (e.code == 'ESRCH') { + // not running + return false + } + if (e.code == 'EPERM') { + // we're don't have enough permissions to signal this process + return true + } + console.log(pid, 'isRunning', e.code) + return false + } + return false +} + +// intent to stop lokinet and don't restart it +var retries = 0 +function stop() { + shuttingDown = true + if (lokinet && lokinet.killed) { + console.warn('lokinet already stopped') + retries++ + if (retries > 3) { + // 3 exits in a row, something isn't dying + // just quit out + process.exit() + } + return + } + log('requesting lokinet be shutdown') + if (lokinet && !lokinet.killed) { + log('sending SIGINT to lokinet', lokinet.pid) + process.kill(lokinet.pid, 'SIGINT') + lokinet.killed = true + // HACK: lokinet on macos can not be killed if rpc port is in use + var monitorTimerStart = Date.now() + var monitorTimer = setInterval(function() { + if (!isPidRunning(lokinet.pid)) { + // launcher can't exit until this interval is cleared + clearInterval(monitorTimer) + } else { + var diff = Date.now() - monitorTimerStart + if (diff > 15 * 1000) { + // reach 15 secs and lokinet is still running + // escalate it + console.error('Lokinet is still running 15s after we intentionally stopped lokinet?') + process.kill(lokinet.pid, 'SIGKILL') + } else + if (diff > 30 * 1000) { + // reach 30 secs and lokinet is still running + // escalate it + console.error('Lokinet is still running 30s after we intentionally killed lokinet?') + var handles = process._getActiveHandles() + console.log('handles', handles.length) + for(var i in handles) { + var handle = handles[i] + console.log(i, 'type', handle._type, handle) + } + console.log('requests', process._getActiveRequests().length) + console.log('forcing exit') + process.exit() + } + } + }, 1000) + // this timer will stop the system from shutting down + /* + setTimeout(function() { + try { + // check to see if still running + process.kill(lokinet.pid, 0) + log('sending SIGKILL to lokinet') + process.kill(lokinet.pid, 'SIGKILL') + } catch(e) { + console.error('Launcher is still running 15s after we intentionally stopped lokinet?') + var handles = process._getActiveHandles() + console.log('handles', handles.length) + for(var i in handles) { + var handle = handles[i] + console.log(i, 'type', handle._type, handle) + } + console.log('requests', process._getActiveRequests().length) + process.exit() + } + }, 15 * 1000) + */ + } +} + +// isRunning covers this too well +function getPID() { + return (lokinet && !lokinet.killed && lokinet.pid)?lokinet.pid:0 +} + +function enableLogging() { + lokinetLogging = true +} + +function disableLogging() { + console.log('Disabling lokinet logging') + lokinetLogging = false +} + +function getLokiNetIP(cb) { + function checkDNS() { + log('lokinet seems to be running, querying', runningConfig.dns.bind) + // where's our DNS server? + //log('RunningConfig says our lokinet\'s DNS is on', runningConfig.dns.bind) + testDNSForLokinet(runningConfig.dns.bind, function(ips) { + //log('lokinet test', ips) + if (ips && ips.length) { + cb(ips[0]) + } else { + // , retrying + console.error('cant communicate with lokinet DNS') + /* + //process.exit() + setTimeout(function() { + getLokiNetIP(cb) + }, 1000) + */ + cb() + } + }) + } + if (runningConfig.api.enabled) { + log('wait for lokinet startup', runningConfig.api) + var url = 'http://'+runningConfig.api.bind+'/' + waitForUrl(url, function() { + checkDNS() + }) + } else { + checkDNS() + } +} + +module.exports = { + startServiceNode : startServiceNode, + startClient : startClient, + checkConfig : checkConfig, + findLokiNetDNS : findLokiNetDNS, + lookup : lookup, + isRunning : isRunning, + stop : stop, + getLokiNetIP : getLokiNetIP, + enableLogging : enableLogging, + disableLogging : disableLogging, + getPID : getPID, + // FIXME: should we allow hooking of log() too? + onMessage : function(data) { + if (lokinetLogging) { + console.log(`lokinet: ${data}`) + } + }, + onError : function(data) { + console.log(`lokineterr: ${data}`) + }, +} diff --git a/contrib/node-monitor/monitor_client.js b/contrib/node-monitor/monitor_client.js new file mode 100644 index 000000000..8b9715fdf --- /dev/null +++ b/contrib/node-monitor/monitor_client.js @@ -0,0 +1,322 @@ +const lokinet = require('./lokinet') // currently using the 0.5 api +const fs = require('fs') +const ping = require ("net-ping") +// horrible library that shells out to use ping... +//const ping = require('ping') + +// score successful actions +// while we monitor snode and loki addresses + +var session = ping.createSession () + +// probably should nuke profiles.dat each round to level the playing field +if (fs.existsSync('profiles.dat')) { + console.log('cleaning router profiles') + fs.unlinkSync('profiles.dat') +} + +// +// start config +// + +// FIXME: take in a binary_path as an option... +var lokinet_config = { + binary_path : '../../lokinet', +// clients will default to i2p.rocks +// bootstrap_url : 'http://206.81.100.174/n-st-1.signed', +// rpc_ip : '127.0.0.1', +// rpc_port : 28082, +// auto_config_test_host: 'www.google.com', +// auto_config_test_port: 80, +// testnet : true, +// verbose : true, +} + +score_time = null +score_total = 0 + +var lokinet_version = 'unknown' + +var known_snodes = [] +var snodes_stats = {} +function addSnode(snode) { + var idx = known_snodes.indexOf(snode) + if (idx == -1) { + known_snodes.push(snode) + function lookupSnode(snode) { + lokinet.lookup(snode, function(records) { + console.log(snode, 'mapped to', records) + // if no response or not found, retry + if (records === undefined || records === null) { + // failure retry + setTimeout(function() { + console.log('retry lookup for', snode) + lookupSnode(snode) + }, 5000) + return + } + if (!records) { + console.log('records is false', records) + return + } + if (!records.length) { + console.log('record is empty array') + return + } + var ip = records[0] + // reset stats + snodes_stats[snode] = { + onlines: 0, + offlines: 0, + } + // trigger monitor update? + var snodeMonitor = setInterval(function() { + /* + ping.sys.probe(ip, function(isAlive){ + if (isAlive) { + //console.log(snode, ip, 'is online') + snodes_stats[snode].onlines++ + score_total++ + } else { + console.log(snode, ip, 'is offline') + snodes_stats[snode].offlines++ + } + }) + */ + session.pingHost(ip, function(err, target) { + if (err) { + console.warn(target, 'ping err', err); + console.log(snode, ip, 'is offline') + snodes_stats[snode].offlines++ + } else { + //console.log (target + ": Alive") + //console.log(snode, ip, 'is online') + snodes_stats[snode].onlines++ + score_total++ + } + }) + }, 1000) + }) + } + lookupSnode(snode) + } +} + +setInterval(function() { + // can be known but not mapped yet... + console.log('snode score card,', known_snodes.length, 'known routers') + for(var snode in snodes_stats) { + var stats = snodes_stats[snode] + var total = stats.onlines + stats.offlines + var onlinePer = (stats.onlines / total) * 100 + console.log(snode, stats, onlinePer + '%') + } +}, 30 * 1000) + +lokinet.onMessage = function(data) { + console.log(`monitor: ${data}`) + var lines = data.split(/\n/) + for(var i in lines) { + var tline = lines[i].trim() + // lokinet-0.4.0-59e6a4bc (dev build) + if (tline.match('lokinet-0.')) { + var parts = tline.split('lokinet-0.') + lokinet_version = parts[1] + console.log('VERSION', parts[1]) + } + if (tline.match('Using config file:')) { + // get bootstrap info + var parts = tline.split('Using config file: ') + console.log('CONFIGFILE', parts[1]) + } + if (tline.match('Added bootstrap node')) { + // get bootstrap info + var parts = tline.split('Added bootstrap node ') + addSnode(parts[1]) + console.log('BOOTSTRAP', parts[1]) + } + if (tline.match('Set Up networking for')) { + // get interface info + var parts = tline.split('Set Up networking for ') + // default:9j4uido1ai7ucirbncbqii1395e8ccd6cjomo9cccp3ztx1ukwio.loki + console.log('INTERFACE', parts[1]) + } + // + if (tline.match('session with ')) { + // potential node + var parts = tline.split('session with ') + if (parts.length) { + var one = parts[1].replace(' established', '') + var two = one.split('.snode') // snode\u001b[0;0m + var snode = two[0] + '.snode' + addSnode(snode) + console.log('SESSION', snode) + score_total++ + } else { + console.log('unknown session line', tline) + } + } + if (tline.match(' routers from exploration')) { + var parts = tline.split('\tgot ') + if (parts.length > 1) { + var routers = parts[1].replace(' routers from exploration', '') + console.log('ROUTERS', routers) + if (routers && routers != '0') { + score_total++ + } + } else { + console.log('unknown exploration line', tline) + } + } + // ? + if (tline.match('Handle DHT message S relayed=0')) { + score_total++ + } + // relay DHT packet + if (tline.match('Handle DHT message R relayed=0')) { + score_total++ + } + // + if (tline.match(' is built, took ')) { + // path TX=8826efd28bede66c59782af9894eed08 RX=a994770a8c6d61700b8ca65e4a3adea3 on OBContext:default:3y3ch3m6c8xgmutxwe8fger6n963hn3mkn6tk14j67w1zdxj1n1y.loki-icxqqcpd3sfkjbqifn53h7rmusqa1fyxwqyfrrcgkd37xcikwa7y.loki is built, took 2321 ms + var parts = tline.split('path TX=') + if (parts.length > 1) { + var two = parts[1].split(' RX=') + if (two.length > 1) { + var tx = two[0] + var three = two[1].split(' on OBContext:') + if (three.length > 1) { + var rx = three[0] + var four = three[1].split(' is built, took ') + if (four.length > 1) { + var context = four[0] + var ms = four[1] + console.log('PATH BUILT', tx, rx, context, ms) + } + } + } + } + score_total++ + } + // happen right after the built, took + //TX=53e833fcbad1395647f198201d4931e5 RX=b4768bc4b91b86b51b513319f443f538 on default:dnhi78pwrn6cd5yz83i83816nqudwym57s5x4bp3j4g5kbeigw5y.loki built latency=291 + if (tline.match(' is built latency ')) { + // path latency info + } + //[WRN] (166) Tue Apr 30 15:26:25 2019 PDT path/pathbuilder.cpp:319 SNode::8ti485iig9q1wd6k9qr6dqmswrqp9hceohcrtwwa7pn6wcxx9wdy.snode failed to select hop 3 + // not a sure but snode extract + if (tline.match('.snode failed to select hop')) { + var parts = tline.split('SNode::') + //console.log('failed to select hop parts', parts) + if (parts.length > 1) { + var two = parts[1].split(' failed to select hop ') + if (two.length) { + var snode = two[0] + var hops = two[1] + //console.log('hopSelectionFailure', snode, hops) + } + } + } + //obtained an exit via nc9y88xsr7cqmpytc6tbo5dbf5c9fa3is5dezmdwxwgfkuhemg3o.snode + if (tline.match('obtained an exit via ')) { + var parts = tline.split('obtained an exit via ') + if (parts.length > 1) { + var two = parts[1].split('.snode') + var snode = two[0]+'.snode' + addSnode(snode) + } + score_total++ + } + //TX=219222f0f390e40ce47745af292001f2 RX=671c259dd9590173019694fc10433b63 on SNode::nc9y88xsr7cqmpytc6tbo5dbf5c9fa3is5dezmdwxwgfkuhemg3o.snode nc9y88xsr7cqmpytc6tbo5dbf5c9fa3is5dezmdwxwgfkuhemg3o.snode Granted exit + if (tline.match('Granted exit')) { + var one = tline.split('TX=') + if (one.length > 1) { + var two = one[1].split(' RX=') + if (two.length > 1) { + var three = two[1].split(' on SNode::') + if (three.length > 1) { + var tx = two[0] + var rx = three[0] + console.log('EXIT', tx, rx, 'rest', three[1]) + } + } + } + score_total++ + } + //service/endpoint.cpp:652 default:kzow69uftho8ukm8g4kf88y5zka84azfrmfx5okkmrg7ucdz5d1o.loki IntroSet publish confirmed + if (tline.match('IntroSet publish confirmed')) { + score_total++ + } + } +} +lokinet.onError = function(data) { + console.log(`monitorerr: ${data}`) + // start on 2019-04-30T15:40:55.540314745-07:00 X Records + // buffer until + // end on \n\n + var lines = data.toString().split(/\n/) + for(var i in lines) { + var tline = lines[i].trim() + if (tline.match('utp.session.sendq.')) { + //console.log('sendq') + var one = tline.split('utp.session.sendq.') + // parts[1] = 8ti485iig9q1wd6k9qr6dqmswrqp9hceohcrtwwa7pn6wcxx9wdy.snode [ + // count = 282, total = 3, min = 0, max = 1 ] + if (one.length > 1) { + var two = one[1].split(' [ ') + if (two.length > 1) { + var snode = two[0] + var three = two[1].split(', ') + if (three.length > 3) { + var count = three[0] + var total = three[1] + var min = three[2] + if (min == 'min = 0') { + //console.log('no bloat') + // not sure if this is related to our version or not... + score_total++ + } + var max = three[3] + console.log('UTPSENDQ', snode, count, total, min, max) + } + } + } + } + } +} + +// +// end config +// + +function checkIP(cb) { + lokinet.getLokiNetIP(function(ip) { + if (ip === undefined) { + checkIP(cb) + return + } + //console.log('lokinet interface ip', ip) + cb(ip) + }) +} + +lokinet.startClient(lokinet_config, function() { + // lokinet isn't necessarily running at this point + console.log('Starting monitor') + score_time = Date.now() + checkIP(function(ip) { + console.log('lokinet interface ip', ip) + lokinet.findLokiNetDNS(function(servers) { + console.log('monitor detected DNS Servers', servers) + }) + setInterval(function() { + console.log('score', score_total, 'time', ((Date.now() - score_time)/1000)) + }, 30000) + // maybe run until a specific time and quit for comparison + // 1200s (two 10min sessions) + setTimeout(function() { + console.log(lokinet_config.binary_path, 'version', lokinet_version, 'final score', score_total) + process.exit() + }, 3 * 600 * 1000) // 3x 10m sessions worth + }) +}) diff --git a/contrib/node-monitor/package-lock.json b/contrib/node-monitor/package-lock.json new file mode 100644 index 000000000..7cbcebfcd --- /dev/null +++ b/contrib/node-monitor/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "lokinet-monitor", + "version": "0.0.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "nan": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", + "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" + }, + "net-ping": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/net-ping/-/net-ping-1.2.3.tgz", + "integrity": "sha512-ZKxj/kVPKL2RIsV9nR6I8nMT8Pi3k6ciTBKxD/6gd5lga9qcNmlyqNv+dbXqYGBvHsmG9yIpsfajr8X054x2fQ==", + "requires": { + "raw-socket": "*" + } + }, + "raw-socket": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/raw-socket/-/raw-socket-1.6.4.tgz", + "integrity": "sha512-ab/By3tp0gTDzwEJxgKUv+uIma0JFVnJElNMKNAqNiET+GQ2VAfR6eUVfnm0yRG/vxGu8gO2BYWhR30sQOTebg==", + "requires": { + "nan": "2.10.*" + } + } + } +} diff --git a/contrib/node-monitor/package.json b/contrib/node-monitor/package.json new file mode 100644 index 000000000..dc464f4f1 --- /dev/null +++ b/contrib/node-monitor/package.json @@ -0,0 +1,25 @@ +{ + "name": "lokinet-monitor", + "version": "0.0.1", + "description": "Lokinet monitor", + "main": "monitor_client.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/loki-project/loki-network.git" + }, + "keywords": [ + "lokinet" + ], + "author": "Ryan Tharp", + "license": "ISC", + "bugs": { + "url": "https://github.com/loki-project/loki-network/issues" + }, + "homepage": "https://github.com/loki-project/loki-network#readme", + "dependencies": { + "net-ping": "^1.2.3" + } +}