mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-05-31 14:50:47 +00:00
* Remove Supervisor lockfile cleanup SIGTERM listener * Modify lockfile.getLocksTaken to read files from the filesystem * Remove in-memory tracking of locks taken in favor of filesystem * Require both `(resin-)updates.lock` to be locked with `nobody` UID for service to count as locked by the Supervisor Signed-off-by: Christina Ying Wang <christina@balena.io>
171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
import _ from 'lodash';
|
|
import { promises as fs, watch } from 'fs';
|
|
import networkCheck from 'network-checker';
|
|
import os from 'os';
|
|
import url from 'url';
|
|
|
|
import * as constants from './lib/constants';
|
|
import { isEEXIST } from './lib/errors';
|
|
import { checkFalsey } from './lib/validation';
|
|
|
|
import blink = require('./lib/blink');
|
|
|
|
import log from './lib/supervisor-console';
|
|
|
|
const networkPattern = {
|
|
blinks: 4,
|
|
pause: 1000,
|
|
};
|
|
|
|
let isConnectivityCheckPaused = false;
|
|
let isConnectivityCheckEnabled = true;
|
|
|
|
function checkHost(
|
|
opts: networkCheck.ConnectOptions,
|
|
): boolean | PromiseLike<boolean> {
|
|
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;
|
|
}
|
|
|
|
export async function isVPNActive(): Promise<boolean> {
|
|
let active: boolean = true;
|
|
try {
|
|
await fs.lstat(`${constants.vpnStatusPath}/active`);
|
|
} catch {
|
|
active = false;
|
|
}
|
|
log.info(`VPN connection is ${active ? 'active' : 'not active'}.`);
|
|
return active;
|
|
}
|
|
|
|
async function vpnStatusInotifyCallback(): Promise<void> {
|
|
isConnectivityCheckPaused = await isVPNActive();
|
|
}
|
|
|
|
export const startConnectivityCheck = _.once(
|
|
async (
|
|
apiEndpoint: string,
|
|
enable: boolean,
|
|
onChangeCallback?: networkCheck.MonitorChangeFunction,
|
|
) => {
|
|
enableConnectivityCheck(enable);
|
|
if (!apiEndpoint) {
|
|
log.debug('No API endpoint specified, skipping connectivity check');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await fs.mkdir(constants.vpnStatusPath);
|
|
} catch (err: any) {
|
|
if (isEEXIST(err)) {
|
|
log.debug('VPN status path exists.');
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
watch(constants.vpnStatusPath, vpnStatusInotifyCallback);
|
|
|
|
if (enable) {
|
|
void vpnStatusInotifyCallback();
|
|
}
|
|
|
|
const parsedUrl = url.parse(apiEndpoint);
|
|
const port = parseInt(parsedUrl.port!, 10);
|
|
|
|
customMonitor(
|
|
{
|
|
host: parsedUrl.hostname ?? undefined,
|
|
port: port || (parsedUrl.protocol === 'https' ? 443 : 80),
|
|
path: parsedUrl.path || '/',
|
|
interval: 10 * 1000,
|
|
},
|
|
(connected) => {
|
|
onChangeCallback?.(connected);
|
|
if (connected) {
|
|
log.info('Internet Connectivity: OK');
|
|
blink.pattern.stop();
|
|
} else {
|
|
log.info('Waiting for connectivity...');
|
|
blink.pattern.start(networkPattern);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
export function enableConnectivityCheck(enable: boolean) {
|
|
// Only disable if value explicitly matches falsey
|
|
enable = !checkFalsey(enable);
|
|
enableCheck(enable);
|
|
log.debug(`Connectivity check enabled: ${enable}`);
|
|
}
|
|
|
|
export const connectivityCheckEnabled = async () => 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 const shouldReportInterface = (intf: string) => !IP_REGEX.test(intf);
|
|
|
|
const shouldReportIPv6 = (ip: os.NetworkInterfaceInfo) =>
|
|
ip.family === 'IPv6' && !ip.internal && ip.scopeid === 0;
|
|
|
|
const shouldReportIPv4 = (ip: os.NetworkInterfaceInfo) =>
|
|
ip.family === 'IPv4' && !ip.internal;
|
|
|
|
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)
|
|
const networkInterfaces = os.networkInterfaces();
|
|
return Object.entries(networkInterfaces)
|
|
.filter(([iface]) => shouldReportInterface(iface))
|
|
.flatMap(([, validInterfaces]) => {
|
|
return (
|
|
validInterfaces
|
|
// Only report valid ipv6 and ipv4 addresses
|
|
?.filter((ip) => shouldReportIPv6(ip) || shouldReportIPv4(ip))
|
|
.map(({ address }) => address) ?? []
|
|
);
|
|
});
|
|
}
|
|
|
|
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();
|
|
};
|
|
}
|