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 @@ - -
+ +