diff --git a/tailscalectl/TailscaleWidget.qml b/tailscalectl/TailscaleWidget.qml index 2ac6bc6..f1a91b1 100644 --- a/tailscalectl/TailscaleWidget.qml +++ b/tailscalectl/TailscaleWidget.qml @@ -86,7 +86,7 @@ PluginComponent { const data = JSON.parse(this.text) root.isConnected = data.BackendState === "Running" root.tailscaleIP = (data.Self?.TailscaleIPs?.[0]) || "" - root.currentExitNode = data.CurrentExitNode?.HostName || "" + root.currentExitNode = TailscaleLib.findActiveExitNode(data.Peer || {}) root.peers = TailscaleLib.parsePeers(data.Peer || {}) } catch (e) { root.isConnected = false diff --git a/tailscalectl/lib.js b/tailscalectl/lib.js index b48dd14..f4cd31e 100644 --- a/tailscalectl/lib.js +++ b/tailscalectl/lib.js @@ -15,6 +15,17 @@ function makeExitNodeCommand(hostname) { return ["tailscale", "set", "--exit-node=" + hostname] } +function findActiveExitNode(peerMap) { + if (!peerMap) return "" + for (const key in peerMap) { + const p = peerMap[key] + if (p.ExitNode) { + return p.HostName || key + } + } + return "" +} + function errorMessage(cmd) { var messages = { "up": "Failed to connect to Tailscale", @@ -29,5 +40,5 @@ function errorMessage(cmd) { // CommonJS export for Node.js tests (ignored by QML) if (typeof module !== "undefined" && module.exports) { - module.exports = { parsePeers, makeExitNodeCommand, errorMessage } + module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage } } diff --git a/test/lib.test.js b/test/lib.test.js index 8c6c435..ce9897b 100644 --- a/test/lib.test.js +++ b/test/lib.test.js @@ -1,7 +1,7 @@ import { test } from "node:test" import assert from "node:assert" import lib from "../tailscalectl/lib.js" -const { parsePeers, makeExitNodeCommand, errorMessage } = lib +const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage } = lib test("parsePeers extracts exitNode from ExitNodeOption", () => { const peerMap = { @@ -35,6 +35,21 @@ test("makeExitNodeCommand with empty string clears exit node", () => { assert.deepStrictEqual(cmd, ["tailscale", "set", "--exit-node="]) }) +test("findActiveExitNode returns hostname of peer with ExitNode=true", () => { + const peerMap = { + "peer-1": { HostName: "gluetun-sjc", ExitNode: true, ExitNodeOption: true }, + "peer-2": { HostName: "gluetun-den", ExitNode: false, ExitNodeOption: true } + } + assert.strictEqual(findActiveExitNode(peerMap), "gluetun-sjc") +}) + +test("findActiveExitNode returns empty string when no exit node", () => { + const peerMap = { + "peer-1": { HostName: "laptop", ExitNode: false, ExitNodeOption: false } + } + assert.strictEqual(findActiveExitNode(peerMap), "") +}) + test("errorMessage returns user-friendly message for tailscale up failure", () => { const msg = errorMessage("up", 1) assert.strictEqual(msg, "Failed to connect to Tailscale")