diff --git a/package-lock.json b/package-lock.json index 7d659f2c..f072fd78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "balena-supervisor", - "version": "8.4.1", + "version": "8.4.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1125,6 +1125,27 @@ "type-is": "~1.6.16" } }, + "bonjour": { + "version": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", + "from": "git+https://github.com/resin-io/bonjour.git#fixed-mdns", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc", + "multicast-dns-service-types": "^1.1.0" + }, + "dependencies": { + "array-flatten": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", + "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=", + "dev": true + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1274,6 +1295,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", @@ -2159,6 +2186,12 @@ } } }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2402,6 +2435,31 @@ "path-type": "^3.0.0" } }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", + "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, "docker-delta": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/docker-delta/-/docker-delta-2.2.2.tgz", @@ -6471,6 +6529,21 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "multicast-dns": { + "version": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc", + "from": "git+https://github.com/resin-io-modules/multicast-dns.git#listen-on-all-interfaces", + "dev": true, + "requires": { + "dns-packet": "^1.0.1", + "thunky": "^0.1.0" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, "mute-stream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", @@ -6565,9 +6638,9 @@ "dev": true }, "network-checker": { - "version": "0.0.6", - "resolved": "http://registry.npmjs.org/network-checker/-/network-checker-0.0.6.tgz", - "integrity": "sha1-165Y5NuVJE0tJnbM1LIlJRf5Mq4=", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/network-checker/-/network-checker-0.1.1.tgz", + "integrity": "sha512-dc/LiwC0pp37njpe8TA+oRa5BWkH8+WyFVY0aeuDZqMLHq4kaLHqWJowEfKBI7KT39vmTapWJN0SoHxhx6aL4A==", "dev": true, "requires": { "bluebird": "^3.0.0", @@ -8193,27 +8266,9 @@ "dev": true, "requires": { "bluebird": "^3.0.0", + "bonjour": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", "ip": "^1.1.4", "lodash": "^4.17.4" - }, - "dependencies": { - "array-flatten": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", - "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=" - }, - "bonjour": { - "version": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", - "from": "git+https://github.com/resin-io/bonjour.git#e018851dc823b4b3f670f658f71d0c1c7f3e637c", - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "git+https://github.com/resin-io-modules/multicast-dns.git#a15c63464eb43e8925b187ed5cb9de6892e8aacc", - "multicast-dns-service-types": "^1.1.0" - } - } } }, "resin-errors": { @@ -10301,6 +10356,12 @@ "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=", "dev": true }, + "thunky": { + "version": "0.1.0", + "resolved": "http://registry.npmjs.org/thunky/-/thunky-0.1.0.tgz", + "integrity": "sha1-vzAUaCTituZ7Dy16Ssi+smkIaE4=", + "dev": true + }, "tildify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.0.0.tgz", diff --git a/package.json b/package.json index 3bd89c2e..bb9bfc71 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "mocha": "^5.1.1", "mochainon": "^2.0.0", "mz": "^2.7.0", - "network-checker": "~0.0.5", + "network-checker": "^0.1.1", "pinejs-client": "^2.4.0", "prettier": "^1.14.3", "register-coffee-coverage": "0.0.1", diff --git a/src/network.coffee b/src/network.coffee deleted file mode 100644 index e90ac483..00000000 --- a/src/network.coffee +++ /dev/null @@ -1,107 +0,0 @@ -Promise = require 'bluebird' -_ = require 'lodash' -url = require 'url' -networkCheck = require 'network-checker' -os = require 'os' -fs = Promise.promisifyAll(require('fs')) - -constants = require './lib/constants' -{ checkTruthy } = require './lib/validation' -blink = require './lib/blink' -{ EEXIST } = require './lib/errors' - -networkPattern = - blinks: 4 - pause: 1000 - -pauseConnectivityCheck = false -enableConnectivityCheck = true - -# options: An object of net.connect options, with the addition of: -# timeout: 10s -checkHost = (options) -> - if !enableConnectivityCheck or pauseConnectivityCheck - return true - else - return networkCheck.checkHost(options) - -# Custom monitor that uses checkHost function above. -customMonitor = (options, fn) -> - networkCheck.monitor(checkHost, options, fn) - -# enable: A Boolean to enable/disable the connectivity checks -exports.enableCheck = enableCheck = (enable) -> - enableConnectivityCheck = enable - -# Call back for inotify triggered when the VPN status is changed. -vpnStatusInotifyCallback = -> - fs.lstatAsync(constants.vpnStatusPath + '/active') - .then -> - pauseConnectivityCheck = true - .catch -> - pauseConnectivityCheck = false - -exports.startConnectivityCheck = _.once (apiEndpoint, enable, onChangeCallback) -> - exports.enableConnectivityCheck(enable) - if !apiEndpoint? - console.log('No API endpoint specified, skipping connectivity check') - return - parsedUrl = url.parse(apiEndpoint) - fs.mkdirAsync(constants.vpnStatusPath) - .catch EEXIST, (err) -> - console.log('VPN status path exists.') - .then -> - fs.watch(constants.vpnStatusPath, vpnStatusInotifyCallback) - - # Manually trigger the call back to detect cases when VPN was switched on before the supervisor starts. - vpnStatusInotifyCallback() if enable - customMonitor - host: parsedUrl.hostname - port: parsedUrl.port ? (if parsedUrl.protocol is 'https:' then 443 else 80) - interval: 10 * 1000 - (connected) -> - onChangeCallback?(connected) - if connected - console.log('Internet Connectivity: OK') - blink.pattern.stop() - else - console.log('Waiting for connectivity...') - blink.pattern.start(networkPattern) - -# Callback function to enable/disable tcp pings -exports.enableConnectivityCheck = (val) -> - enabled = checkTruthy(val) ? true - enableCheck(enabled) - console.log("Connectivity check enabled: #{enabled}") - -exports.connectivityCheckEnabled = Promise.method -> - return enableConnectivityCheck - -exports.getIPAddresses = -> - # We get IP addresses but ignore: - # - docker and balena bridges (docker0, docker1, balena0, etc) - # - legacy rce bridges (rce0, etc) - # - tun interfaces like the legacy vpn - # - the resin VPN interface (resin-vpn) - # - loopback interface (lo) - # - the bridge for dnsmasq (resin-dns) - # - the docker network for the supervisor API (supervisor0) - # - custom docker network bridges (br- + 12 hex characters) - _.flatten(_.map(_.omitBy(os.networkInterfaces(), (interfaceFields, interfaceName) -> - /^(?:balena|docker|rce|tun)[0-9]+|tun[0-9]+|resin-vpn|lo|resin-dns|supervisor0|balena-redsocks|resin-redsocks|br-[0-9a-f]{12}$/.test(interfaceName)) - , (validInterfaces) -> - _.map(_.pickBy(validInterfaces, family: 'IPv4'), 'address')) - ) - -exports.startIPAddressUpdate = do -> - _lastIPValues = null - return (callback, interval) -> - getAndReportIP = -> - ips = exports.getIPAddresses() - if !_.isEmpty(_.xor(ips , _lastIPValues)) - _lastIPValues = ips - callback(ips) - setInterval( -> - getAndReportIP() - , interval) - getAndReportIP() diff --git a/src/network.ts b/src/network.ts new file mode 100644 index 00000000..82248e5f --- /dev/null +++ b/src/network.ts @@ -0,0 +1,156 @@ +import * as Bluebird from 'bluebird'; +import * as _ from 'lodash'; +import { fs } from 'mz'; +import * as networkCheck from 'network-checker'; +import * as os from 'os'; +import * as url from 'url'; + +import * as constants from './lib/constants'; +import { EEXIST } from './lib/errors'; +import { checkTruthy } from './lib/validation'; + +import blink = require('./lib/blink'); + +const networkPattern = { + blinks: 4, + pause: 1000, +}; + +let isConnectivityCheckPaused = false; +let isConnectivityCheckEnabled = true; + +function checkHost( + opts: networkCheck.ConnectOptions, +): boolean | PromiseLike { + return ( + !isConnectivityCheckEnabled || + isConnectivityCheckPaused || + networkCheck.checkHost(opts) + ); +} + +function customMonitor( + options: networkCheck.ConnectOptions, + fn: networkCheck.MonitorChangeFunction, +) { + return networkCheck.monitor(checkHost, options, fn); +} + +export function enableCheck(enable: boolean) { + isConnectivityCheckEnabled = enable; +} + +async function vpnStatusInotifyCallback(): Promise { + try { + await fs.lstat(constants.vpnStatusPath); + isConnectivityCheckPaused = true; + } catch { + isConnectivityCheckPaused = false; + } +} + +export const startConnectivityCheck = _.once( + async ( + apiEndpoint: string, + enable: boolean, + onChangeCallback?: networkCheck.MonitorChangeFunction, + ) => { + enableConnectivityCheck(enable); + if (!apiEndpoint) { + console.log('No API endpoint specified, skipping connectivity check'); + return; + } + + await Bluebird.resolve(fs.mkdir(constants.vpnStatusPath)) + .catch(EEXIST, () => { + console.log('VPN status path exists.'); + }) + .then(() => { + fs.watch(constants.vpnStatusPath, vpnStatusInotifyCallback); + }); + + if (enable) { + vpnStatusInotifyCallback(); + } + + const parsedUrl = url.parse(apiEndpoint); + const port = parseInt(parsedUrl.port!, 10); + + customMonitor( + { + host: parsedUrl.hostname, + port: port || (parsedUrl.protocol === 'https' ? 443 : 80), + path: parsedUrl.path || '/', + interval: 10 * 1000, + }, + connected => { + if (_.isFunction(onChangeCallback)) { + onChangeCallback(connected); + } + if (connected) { + console.log('Internet Connectivity: OK'); + blink.pattern.stop(); + } else { + console.log('Waiting for connectivity...'); + blink.pattern.start(networkPattern); + } + }, + ); + }, +); + +export function enableConnectivityCheck(enable: boolean) { + const boolEnable = checkTruthy(enable); + enable = boolEnable != null ? boolEnable : true; + enableCheck(enable); + console.log(`Connectivity check enabled: ${enable}`); +} + +export const connectivityCheckEnabled = Bluebird.method( + () => isConnectivityCheckEnabled, +); + +const IP_REGEX = /^(?:balena|docker|rce|tun)[0-9]+|tun[0-9]+|resin-vpn|lo|resin-dns|supervisor0|balena-redsocks|resin-redsocks|br-[0-9a-f]{12}$/; +export function getIPAddresses(): string[] { + // We get IP addresses but ignore: + // - docker and balena bridges (docker0, docker1, balena0, etc) + // - legacy rce bridges (rce0, etc) + // - tun interfaces like the legacy vpn + // - the resin VPN interface (resin-vpn) + // - loopback interface (lo) + // - the bridge for dnsmasq (resin-dns) + // - the docker network for the supervisor API (supervisor0) + // - custom docker network bridges (br- + 12 hex characters) + return _(os.networkInterfaces()) + .omitBy((_interfaceFields, interfaceName) => IP_REGEX.test(interfaceName)) + .flatMap(validInterfaces => { + return _(validInterfaces) + .pickBy({ family: 'IPv4' }) + .map('address') + .value(); + }) + .value(); +} + +export function startIPAddressUpdate(): ( + callback: (ips: string[]) => void, + interval: number, +) => void { + let lastIPValues: string[] | null = null; + return (cb, interval) => { + const getAndReportIP = () => { + const ips = getIPAddresses(); + if ( + !_(ips) + .xor(lastIPValues) + .isEmpty() + ) { + lastIPValues = ips; + cb(ips); + } + }; + + setInterval(getAndReportIP, interval); + getAndReportIP(); + }; +} diff --git a/typings/blinking.d.ts b/typings/blinking.d.ts index e0a5e816..de11c5df 100644 --- a/typings/blinking.d.ts +++ b/typings/blinking.d.ts @@ -11,7 +11,7 @@ declare module 'blinking' { stop: () => void; } - function blinking(ledFile: string): Blink; + function blinking(ledFile: string): { pattern: Blink }; export = blinking; }