Add exit nodes to the panel

This commit is contained in:
David Heinemeier Hansson
2026-05-27 08:44:32 +02:00
parent 67dd7a2953
commit 8350a792ac
4 changed files with 45 additions and 25 deletions
+27 -14
View File
@@ -52,6 +52,21 @@ function accountLabel(account) {
return String(account.id || "Unknown account") return String(account.id || "Unknown account")
} }
function peerFromStatus(id, peer) {
return {
id: id,
HostName: displayHostName(peer.HostName, peer.DNSName),
DNSName: cleanDnsName(peer.DNSName),
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
TailscaleIPv6: filterIPv6(peer.TailscaleIPs || []),
Online: peer.Online === true,
OS: String(peer.OS || ""),
Tags: peer.Tags || [],
ExitNodeOption: peer.ExitNodeOption === true,
ExitNode: peer.ExitNode === true
}
}
function parseStatus(raw) { function parseStatus(raw) {
var text = String(raw || "").trim() var text = String(raw || "").trim()
if (text === "") return { ok: true, unavailable: true, message: "Disconnected" } if (text === "") return { ok: true, unavailable: true, message: "Disconnected" }
@@ -62,28 +77,24 @@ function parseStatus(raw) {
var self = data.Self || {} var self = data.Self || {}
var selfIps = filterIPv4(self.TailscaleIPs || data.TailscaleIPs || []) var selfIps = filterIPv4(self.TailscaleIPs || data.TailscaleIPs || [])
var peers = [] var peers = []
var exitNodes = []
var rawPeers = data.Peer || {} var rawPeers = data.Peer || {}
for (var id in rawPeers) { for (var id in rawPeers) {
var peer = rawPeers[id] || {} var peer = rawPeers[id] || {}
if (peer.Online !== true) continue var normalized = peerFromStatus(id, peer)
peers.push({ if (normalized.Online) {
id: id, peers.push(normalized)
HostName: displayHostName(peer.HostName, peer.DNSName), if (normalized.ExitNodeOption) exitNodes.push(normalized)
DNSName: cleanDnsName(peer.DNSName), }
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
TailscaleIPv6: filterIPv6(peer.TailscaleIPs || []),
Online: true,
OS: String(peer.OS || ""),
Tags: peer.Tags || [],
ExitNodeOption: peer.ExitNodeOption === true,
ExitNode: peer.ExitNode === true
})
} }
peers.sort(function(a, b) { peers.sort(function(a, b) {
return String(a.HostName).localeCompare(String(b.HostName)) return String(a.HostName).localeCompare(String(b.HostName))
}) })
exitNodes.sort(function(a, b) {
return String(a.HostName).localeCompare(String(b.HostName))
})
return { return {
ok: true, ok: true,
@@ -95,7 +106,8 @@ function parseStatus(raw) {
selfName: displayHostName(self.HostName, self.DNSName), selfName: displayHostName(self.HostName, self.DNSName),
selfDnsName: cleanDnsName(self.DNSName), selfDnsName: cleanDnsName(self.DNSName),
selfIp: selfIps.length > 0 ? selfIps[0] : "", selfIp: selfIps.length > 0 ? selfIps[0] : "",
peers: peers peers: peers,
exitNodes: exitNodes
} }
} catch (e) { } catch (e) {
return { ok: false, unavailable: true, message: "Status error", error: "Failed to parse tailscale status" } return { ok: false, unavailable: true, message: "Status error", error: "Failed to parse tailscale status" }
@@ -143,6 +155,7 @@ if (typeof module !== "undefined") {
displayHostName: displayHostName, displayHostName: displayHostName,
osIcon: osIcon, osIcon: osIcon,
accountLabel: accountLabel, accountLabel: accountLabel,
peerFromStatus: peerFromStatus,
parseStatus: parseStatus, parseStatus: parseStatus,
parseAccounts: parseAccounts parseAccounts: parseAccounts
} }
+2 -9
View File
@@ -39,14 +39,7 @@ Panel {
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
readonly property bool showConnections: tailscale.accounts.length > 1 || tailscale.accountsAccessDenied readonly property bool showConnections: tailscale.accounts.length > 1 || tailscale.accountsAccessDenied
readonly property bool showPeers: tailscale.running && tailscale.peers.length > 0 readonly property bool showPeers: tailscale.running && tailscale.peers.length > 0
readonly property var exitNodes: { readonly property var exitNodes: tailscale.exitNodes
var nodes = []
for (var i = 0; i < tailscale.peers.length; i++) {
var peer = tailscale.peers[i]
if (peer && peer.ExitNodeOption === true) nodes.push(peer)
}
return nodes
}
readonly property bool showExitNodes: tailscale.running && exitNodes.length > 0 readonly property bool showExitNodes: tailscale.running && exitNodes.length > 0
readonly property color iconColor: tailscale.running ? foreground : dim readonly property color iconColor: tailscale.running ? foreground : dim
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent" readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
@@ -877,7 +870,7 @@ Panel {
Text { Text {
id: exitNodeGlyph id: exitNodeGlyph
text: tailscale.osIcon(peer ? peer.OS : "") text: "󱇢"
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode ? root.foreground : root.dim color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode ? root.foreground : root.dim
font.family: root.fontFamily font.family: root.fontFamily
font.pixelSize: Style.font.body font.pixelSize: Style.font.body
+6 -2
View File
@@ -20,6 +20,7 @@ Item {
property string selfIp: "" property string selfIp: ""
property string authUrl: "" property string authUrl: ""
property var peers: [] property var peers: []
property var exitNodes: []
property var accounts: [] property var accounts: []
property string selectedAccountId: "" property string selectedAccountId: ""
property string selectedAccountLabel: "" property string selectedAccountLabel: ""
@@ -158,6 +159,7 @@ Item {
selfIp = "" selfIp = ""
authUrl = "" authUrl = ""
peers = [] peers = []
exitNodes = []
accounts = [] accounts = []
selectedAccountId = "" selectedAccountId = ""
selectedAccountLabel = "" selectedAccountLabel = ""
@@ -188,6 +190,7 @@ Item {
selfDnsName = parsed.selfDnsName selfDnsName = parsed.selfDnsName
selfIp = parsed.selfIp selfIp = parsed.selfIp
peers = parsed.running ? parsed.peers : [] peers = parsed.running ? parsed.peers : []
exitNodes = parsed.running ? parsed.exitNodes : []
if (needsLogin) statusText = "Needs login" if (needsLogin) statusText = "Needs login"
else if (running) { else if (running) {
@@ -253,8 +256,9 @@ Item {
function setExitNode(peer) { function setExitNode(peer) {
if (!installed || !running || !peer || exitNodeProcess.running) return if (!installed || !running || !peer || exitNodeProcess.running) return
var target = exitNodeTarget(peer) var active = peer.ExitNode === true
if (target === "") return var target = active ? "" : exitNodeTarget(peer)
if (!active && target === "") return
_exitNodeOutput = "" _exitNodeOutput = ""
_exitNodeError = "" _exitNodeError = ""
settingExitNodeId = String(peer.id || "") settingExitNodeId = String(peer.id || "")
+10
View File
@@ -47,6 +47,15 @@ const status = tailscale.parseStatus(JSON.stringify({
Online: false, Online: false,
OS: 'linux' OS: 'linux'
}, },
offlineExit: {
HostName: 'mbu-ser9',
DNSName: 'mbu-ser9.tail32f559.ts.net.',
TailscaleIPs: ['100.125.28.77', 'fd7a:115c:a1e0::1037:1c4d'],
Online: false,
OS: 'linux',
ExitNodeOption: true,
ExitNode: false
},
onlineA: { onlineA: {
HostName: 'alpha', HostName: 'alpha',
DNSName: 'alpha.tail32f559.ts.net.', DNSName: 'alpha.tail32f559.ts.net.',
@@ -62,6 +71,7 @@ assertEqual(status.selfIp, '100.74.97.73', 'tailscale parses self IP')
assertDeepEqual(status.peers.map(peer => peer.HostName), ['alpha', 'zed'], 'tailscale filters offline peers and sorts online peers') assertDeepEqual(status.peers.map(peer => peer.HostName), ['alpha', 'zed'], 'tailscale filters offline peers and sorts online peers')
assertDeepEqual(status.peers[0].TailscaleIPv6, ['fd7a:115c:a1e0::1901:334b'], 'tailscale preserves peer IPv6 addresses for copy menu') assertDeepEqual(status.peers[0].TailscaleIPv6, ['fd7a:115c:a1e0::1901:334b'], 'tailscale preserves peer IPv6 addresses for copy menu')
assert(status.peers[1].ExitNodeOption && status.peers[1].ExitNode, 'tailscale preserves exit node flags') assert(status.peers[1].ExitNodeOption && status.peers[1].ExitNode, 'tailscale preserves exit node flags')
assertDeepEqual(status.exitNodes.map(peer => peer.HostName), ['zed'], 'tailscale lists only online exit nodes')
const stopped = tailscale.parseStatus(JSON.stringify({ const stopped = tailscale.parseStatus(JSON.stringify({
BackendState: 'Stopped', BackendState: 'Stopped',