From 1d9a28e9abf7628409db7428f1c30acd43aef0cd Mon Sep 17 00:00:00 2001 From: SGprooo Date: Wed, 19 Jul 2023 20:58:21 +0800 Subject: [PATCH] feat: Tailscale ping monitor (#3178) * Add boilerplate for tailscale ping * tailscale initial commit draft * Refactor TailscalePing & better error handling Split check function into two methods and added async/await syntax for readability/modularity Switched to promise-based error handling (takes into account different types of error such as "Execution error", "Error in output", "no matching peer", and "is local Tailscale IP") and throws them as JavaScript errors. * Minor update * minor update (again) * Update server/monitor-types/tailscale-ping.js Co-authored-by: Frank Elsinga * Update server/monitor-types/tailscale-ping.js Co-authored-by: Frank Elsinga * Update server/monitor-types/tailscale-ping.js Co-authored-by: Frank Elsinga * Update server/monitor-types/tailscale-ping.js Co-authored-by: Frank Elsinga * timeout revision * JSDoc * Removed long explainers * eslint tailscale-ping.js --fix * reran eslint * Fix: Use hostname rather than url * Fixed NaN on monitor interval now interval value is correctly passed to runTailscalePing * Add warning message --------- Co-authored-by: Louis Lam Co-authored-by: Frank Elsinga --- server/monitor-types/tailscale-ping.js | 95 ++++++++++++++++++++++++++ server/uptime-kuma-server.js | 2 + src/lang/en.json | 1 + src/pages/EditMonitor.vue | 11 ++- 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 server/monitor-types/tailscale-ping.js diff --git a/server/monitor-types/tailscale-ping.js b/server/monitor-types/tailscale-ping.js new file mode 100644 index 000000000..eeec9e3f3 --- /dev/null +++ b/server/monitor-types/tailscale-ping.js @@ -0,0 +1,95 @@ +const { MonitorType } = require("./monitor-type"); +const { UP, log } = require("../../src/util"); +const exec = require("child_process").exec; + +/** + * A TailscalePing class extends the MonitorType. + * It runs Tailscale ping to monitor the status of a specific node. + */ +class TailscalePing extends MonitorType { + + name = "tailscale-ping"; + + /** + * Checks the ping status of the URL associated with the monitor. + * It then parses the Tailscale ping command output to update the heatrbeat. + * + * @param {Object} monitor - The monitor object associated with the check. + * @param {Object} heartbeat - The heartbeat object to update. + * @throws Will throw an error if checking Tailscale ping encounters any error + */ + async check(monitor, heartbeat) { + try { + let tailscaleOutput = await this.runTailscalePing(monitor.hostname, monitor.interval); + this.parseTailscaleOutput(tailscaleOutput, heartbeat); + } catch (err) { + log.debug("Tailscale", err); + // trigger log function somewhere to display a notification or alert to the user (but how?) + throw new Error(`Error checking Tailscale ping: ${err}`); + } + } + + /** + * Runs the Tailscale ping command to the given URL. + * + * @param {string} hostname - The hostname to ping. + * @returns {Promise} - A Promise that resolves to the output of the Tailscale ping command + * @throws Will throw an error if the command execution encounters any error. + */ + async runTailscalePing(hostname, interval) { + let cmd = `tailscale ping ${hostname}`; + + log.debug("Tailscale", cmd); + + return new Promise((resolve, reject) => { + let timeout = interval * 1000 * 0.8; + exec(cmd, { timeout: timeout }, (error, stdout, stderr) => { + // we may need to handle more cases if tailscale reports an error that isn't necessarily an error (such as not-logged in or DERP health-related issues) + if (error) { + reject(`Execution error: ${error.message}`); + return; + } + if (stderr) { + reject(`Error in output: ${stderr}`); + return; + } + + resolve(stdout); + }); + }); + } + + /** + * Parses the output of the Tailscale ping command to update the heartbeat. + * + * @param {string} tailscaleOutput - The output of the Tailscale ping command. + * @param {Object} heartbeat - The heartbeat object to update. + * @throws Will throw an eror if the output contains any unexpected string. + */ + parseTailscaleOutput(tailscaleOutput, heartbeat) { + let lines = tailscaleOutput.split("\n"); + + for (let line of lines) { + if (line.includes("pong from")) { + heartbeat.status = UP; + let time = line.split(" in ")[1].split(" ")[0]; + heartbeat.ping = parseInt(time); + heartbeat.msg = line; + break; + } else if (line.includes("timed out")) { + throw new Error(`Ping timed out: "${line}"`); + // Immediately throws upon "timed out" message, the server is expected to re-call the check function + } else if (line.includes("no matching peer")) { + throw new Error(`Nonexistant or inaccessible due to ACLs: "${line}"`); + } else if (line.includes("is local Tailscale IP")) { + throw new Error(`Tailscale only works if used on other machines: "${line}"`); + } else if (line !== "") { + throw new Error(`Unexpected output: "${line}"`); + } + } + } +} + +module.exports = { + TailscalePing, +}; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index da86f3b9e..b206f9a0e 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -99,6 +99,7 @@ class UptimeKumaServer { // Set Monitor Types UptimeKumaServer.monitorTypeList["real-browser"] = new RealBrowserMonitorType(); + UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); this.io = new Server(this.httpServer); } @@ -345,3 +346,4 @@ module.exports = { // Must be at the end to avoid circular dependencies const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor-type"); +const { TailscalePing } = require("./monitor-types/tailscale-ping"); diff --git a/src/lang/en.json b/src/lang/en.json index 2766591f2..e61f87528 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -364,6 +364,7 @@ "deleteDockerHostMsg": "Are you sure want to delete this docker host for all monitors?", "socket": "Socket", "tcp": "TCP / HTTP", + "tailscalePingWarning": "In order to use the Tailscale Ping monitor, you need to install Uptime Kuma without Docker and also install Tailscale client on your server.", "Docker Container": "Docker Container", "Container Name / ID": "Container Name / ID", "Docker Host": "Docker Host", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 0ffef8fe1..a47073d66 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -82,10 +82,17 @@ + + +
@@ -221,8 +228,8 @@ - -
+ +