Skip to content

Commit 536ed25

Browse files
author
cgossiaux
committed
feat(tailscale): add login/authentication support with custom login server
1 parent 2fc53b4 commit 536ed25

12 files changed

Lines changed: 194 additions & 24 deletions

File tree

tailscale/BarWidget.qml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,20 @@ Item {
8989
id: contextMenu
9090

9191
model: [
92+
{
93+
"label": pluginApi?.tr("context.login"),
94+
"action": "login",
95+
"icon": "login",
96+
"visible": mainInstance?.needsLogin ?? false
97+
},
9298
{
9399
"label": (mainInstance?.tailscaleRunning ?? false)
94100
? pluginApi?.tr("context.disconnect")
95101
: pluginApi?.tr("context.connect"),
96102
"action": "toggle-tailscale",
97103
"icon": (mainInstance?.tailscaleRunning ?? false) ? "plug-x" : "plug",
98-
"enabled": mainInstance?.tailscaleInstalled ?? false
104+
"enabled": mainInstance?.tailscaleInstalled ?? false,
105+
"visible": !(mainInstance?.needsLogin ?? false)
99106
},
100107
{
101108
"label": pluginApi?.tr("actions.widget-settings"),
@@ -114,6 +121,10 @@ Item {
114121
if (mainInstance) {
115122
mainInstance.toggleTailscale()
116123
}
124+
} else if (action === "login") {
125+
if (mainInstance) {
126+
mainInstance.loginTailscale()
127+
}
117128
}
118129
}
119130
}

tailscale/Main.qml

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Item {
5555
property bool tailscaleRunning: false
5656
property string tailscaleIp: ""
5757
property string tailscaleStatus: ""
58+
property bool needsLogin: false
5859
property int peerCount: 0
5960
property bool isRefreshing: false
6061
property string lastToggleAction: ""
@@ -153,8 +154,15 @@ Item {
153154
try {
154155
var data = JSON.parse(stdout);
155156
root.tailscaleRunning = data.BackendState === "Running";
157+
root.needsLogin = data.BackendState === "NeedsLogin";
156158

157-
if (root.tailscaleRunning && data.Self && data.Self.TailscaleIPs && data.Self.TailscaleIPs.length > 0) {
159+
if (root.needsLogin) {
160+
root.tailscaleIp = "";
161+
root.tailscaleStatus = "NeedsLogin";
162+
root.peerCount = 0;
163+
root._realPeerList = [];
164+
root.exitNodeStatus = null;
165+
} else if (root.tailscaleRunning && data.Self && data.Self.TailscaleIPs && data.Self.TailscaleIPs.length > 0) {
158166
root.tailscaleIp = filterIPv4(data.Self.TailscaleIPs)[0] || data.Self.TailscaleIPs[0];
159167
root.tailscaleStatus = "Connected";
160168

@@ -198,11 +206,13 @@ Item {
198206
} catch (e) {
199207
Logger.e("Tailscale", "Failed to parse status: " + e);
200208
root.tailscaleRunning = false;
209+
root.needsLogin = false;
201210
root.tailscaleStatus = "Error";
202211
root._realPeerList = [];
203212
}
204213
} else {
205214
root.tailscaleRunning = false;
215+
root.needsLogin = false;
206216
root.tailscaleStatus = "Disconnected";
207217
root.tailscaleIp = "";
208218
root.peerCount = 0;
@@ -236,6 +246,50 @@ Item {
236246

237247
property string lastExitNodeAction: ""
238248

249+
property bool _loginUrlOpened: false
250+
251+
Process {
252+
id: loginProcess
253+
254+
function _handleLine(data) {
255+
if (root._loginUrlOpened) return;
256+
var line = data.trim();
257+
Logger.d("Tailscale", "Login output: " + line);
258+
var urlMatch = line.match(/https?:\/\/\S+/);
259+
if (urlMatch) {
260+
root._loginUrlOpened = true;
261+
Qt.openUrlExternally(urlMatch[0]);
262+
ToastService.showNotice(
263+
pluginApi?.tr("toast.title"),
264+
pluginApi?.tr("toast.login-browser-opened"),
265+
"external-link"
266+
);
267+
}
268+
}
269+
270+
stdout: SplitParser {
271+
onRead: data => loginProcess._handleLine(data)
272+
}
273+
274+
stderr: SplitParser {
275+
onRead: data => loginProcess._handleLine(data)
276+
}
277+
278+
onExited: function (exitCode, exitStatus) {
279+
Logger.d("Tailscale", "Login exited (code " + exitCode + "), urlOpened=" + root._loginUrlOpened);
280+
if (exitCode !== 0 && !root._loginUrlOpened) {
281+
ToastService.showError(
282+
pluginApi?.tr("toast.title"),
283+
pluginApi?.tr("toast.login-failed"),
284+
"alert-circle"
285+
);
286+
Logger.e("Tailscale", "Login failed (exit " + exitCode + ")");
287+
}
288+
root._loginUrlOpened = false;
289+
statusDelayTimer.start();
290+
}
291+
}
292+
239293
// ─── Taildrop state ──────────────────────────────────────────────────────
240294

241295
// Possible values: "idle", "receiving", "sending", "error"
@@ -455,6 +509,7 @@ Item {
455509
function updateTailscaleStatus() {
456510
if (!root.tailscaleInstalled) {
457511
root.tailscaleRunning = false;
512+
root.needsLogin = false;
458513
root.tailscaleIp = "";
459514
root.tailscaleStatus = "Not installed";
460515
root.peerCount = 0;
@@ -480,6 +535,18 @@ Item {
480535
toggleProcess.running = true;
481536
}
482537

538+
function loginTailscale() {
539+
if (!root.tailscaleInstalled)
540+
return;
541+
var loginServer = pluginApi?.pluginSettings?.loginServer || "";
542+
var cmd = ["tailscale", "up", "--accept-routes"];
543+
if (loginServer.trim() !== "") {
544+
cmd.push("--login-server=" + loginServer.trim());
545+
}
546+
loginProcess.command = cmd;
547+
loginProcess.running = true;
548+
}
549+
483550
Timer {
484551
id: updateTimer
485552
interval: refreshInterval
@@ -519,14 +586,19 @@ Item {
519586
"running": root.tailscaleRunning,
520587
"ip": root.tailscaleIp,
521588
"status": root.tailscaleStatus,
522-
"peers": root.peerCount
589+
"peers": root.peerCount,
590+
"needsLogin": root.needsLogin
523591
};
524592
}
525593

526594
function refresh() {
527595
updateTailscaleStatus();
528596
}
529597

598+
function login() {
599+
loginTailscale();
600+
}
601+
530602
// Taildrop IPC: qs ipc call plugin:tailscale receive
531603
function receive() {
532604
startTaildropReceive();

tailscale/Panel.qml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ Item {
353353
NText {
354354
text: mainInstance?.tailscaleRunning
355355
? (mainInstance?.peerList?.length || 0) + " " + (pluginApi?.tr("panel.peers"))
356-
: pluginApi?.tr("panel.not-connected")
356+
: (mainInstance?.needsLogin ? pluginApi?.tr("panel.not-authenticated") : pluginApi?.tr("panel.not-connected"))
357357
pointSize: Style.fontSizeS
358358
color: Color.mOnSurfaceVariant
359359
}
@@ -625,7 +625,23 @@ Item {
625625

626626
NButton {
627627
Layout.fillWidth: true
628-
text: mainInstance?.tailscaleRunning
628+
visible: mainInstance?.needsLogin ?? false
629+
text: pluginApi?.tr("context.login")
630+
icon: "login"
631+
backgroundColor: Color.mPrimary
632+
textColor: Color.mOnPrimary
633+
enabled: mainInstance?.tailscaleInstalled ?? false
634+
onClicked: {
635+
if (mainInstance) {
636+
mainInstance.loginTailscale()
637+
}
638+
}
639+
}
640+
641+
NButton {
642+
Layout.fillWidth: true
643+
visible: !(mainInstance?.needsLogin ?? false)
644+
text: mainInstance?.tailscaleRunning
629645
? pluginApi?.tr("context.disconnect")
630646
: pluginApi?.tr("context.connect")
631647
icon: mainInstance?.tailscaleRunning ? "plug-x" : "plug"

tailscale/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Right-click any online peer in the panel and choose **Send File**. A file picker
7272
| `taildropEnabled` | `true` | Enable Taildrop send/receive. When false, hides the Receive button and Send File option. |
7373
| `taildropDownloadDir` | `"~/Downloads"` | Directory where received files are saved |
7474
| `taildropReceiveMode` | `"operator"` | Taildrop receive mode: `operator` or `pkexec` |
75+
| `loginServer` | `""` | Custom login server URL (e.g. Headscale). Leave empty for default Tailscale. |
7576

7677
## IPC Commands
7778

@@ -90,6 +91,7 @@ qs -c noctalia-shell ipc call plugin:tailscale <command>
9091
| `togglePanel` | Toggle Tailscale panel | `qs -c noctalia-shell ipc call plugin:tailscale togglePanel` |
9192
| `status` | Get current Tailscale status | `qs -c noctalia-shell ipc call plugin:tailscale status` |
9293
| `refresh` | Force refresh Tailscale status | `qs -c noctalia-shell ipc call plugin:tailscale refresh` |
94+
| `login` | Trigger Tailscale login (opens browser) | `qs -c noctalia-shell ipc call plugin:tailscale login` |
9395
| `receive` | Fetch any pending Taildrop files | `qs -c noctalia-shell ipc call plugin:tailscale receive` |
9496

9597
## Usage

tailscale/Settings.qml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ ColumnLayout {
7575
pluginApi?.manifest?.metadata?.defaultSettings?.taildropReceiveMode ||
7676
"operator"
7777

78+
property string editLoginServer:
79+
pluginApi?.pluginSettings?.loginServer ||
80+
pluginApi?.manifest?.metadata?.defaultSettings?.loginServer ||
81+
""
82+
7883
spacing: Style.marginM
7984

8085
// Title section
@@ -185,6 +190,26 @@ ColumnLayout {
185190
onToggled: checked => root.editHideMullvadExitNodes = checked
186191
}
187192

193+
// Authentication section
194+
NDivider {
195+
Layout.fillWidth: true
196+
Layout.topMargin: Style.marginM
197+
Layout.bottomMargin: Style.marginM
198+
}
199+
200+
NLabel {
201+
label: pluginApi?.tr("settings.authentication")
202+
}
203+
204+
NTextInput {
205+
Layout.fillWidth: true
206+
label: pluginApi?.tr("settings.login-server")
207+
description: pluginApi?.tr("settings.login-server-desc")
208+
placeholderText: "https://login.tailscale.com"
209+
text: root.editLoginServer
210+
onTextChanged: root.editLoginServer = text
211+
}
212+
188213
// Terminal section
189214
NDivider {
190215
Layout.fillWidth: true
@@ -316,6 +341,7 @@ ColumnLayout {
316341
pluginApi.pluginSettings.taildropEnabled = root.editTaildropEnabled
317342
pluginApi.pluginSettings.taildropDownloadDir = root.editTaildropDownloadDir
318343
pluginApi.pluginSettings.taildropReceiveMode = root.editTaildropReceiveMode
344+
pluginApi.pluginSettings.loginServer = root.editLoginServer
319345

320346
pluginApi.saveSettings()
321347

tailscale/i18n/de.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
"taildrop-receive-mode": "Empfangsmodus",
3535
"taildrop-receive-mode-desc": "Wie 'tailscale file get' ausgeführt wird. Der Operator-Modus erfordert keine Eingabeaufforderung, wenn 'sudo tailscale set --operator $USER' ausgeführt wurde.",
3636
"taildrop-receive-mode-operator": "Operator (keine Eingabeaufforderung)",
37-
"taildrop-receive-mode-pkexec": "pkexec (jedes Mal Eingabeaufforderung)"
37+
"taildrop-receive-mode-pkexec": "pkexec (jedes Mal Eingabeaufforderung)",
38+
"authentication": "Authentifizierung",
39+
"login-server": "Anmeldeserver",
40+
"login-server-desc": "Benutzerdefinierte Anmeldeserver-URL (z. B. Headscale). Leer lassen für den Standard-Tailscale-Server."
3841
},
3942
"actions": {
4043
"widget-settings": "Widget Einstellungen"
@@ -49,7 +52,8 @@
4952
"ssh": "SSH zum Host",
5053
"ping": "Host anpingen",
5154
"use-exit-node": "Als Ausgangsknoten verwenden",
52-
"send-file": "Datei senden"
55+
"send-file": "Datei senden",
56+
"login": "Anmelden"
5357
},
5458
"toast": {
5559
"title": "Tailscale",
@@ -66,12 +70,15 @@
6670
"terminal-not-configured": {
6771
"title": "Terminal nicht konfiguriert",
6872
"message": "Bitte leg einen Terminalbefehl in den Plugin Einstellungen fest"
69-
}
73+
},
74+
"login-browser-opened": "Authentifizierungsseite im Browser geöffnet",
75+
"login-failed": "Anmeldung fehlgeschlagen"
7076
},
7177
"panel": {
7278
"title": "Tailscale Netzwerk",
7379
"peers": "Peers",
7480
"not-connected": "Nicht verbunden",
81+
"not-authenticated": "Nicht authentifiziert",
7582
"no-peers": "Keine verbundenen Peers",
7683
"admin-console": "Admin Konsole",
7784
"terminal-warning": {

tailscale/i18n/en.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
"taildrop-receive-mode": "Receive Mode",
3535
"taildrop-receive-mode-desc": "How to run 'tailscale file get'. Operator mode requires no prompts if you have run 'sudo tailscale set --operator $USER'.",
3636
"taildrop-receive-mode-operator": "Operator (no prompt)",
37-
"taildrop-receive-mode-pkexec": "pkexec (prompt each time)"
37+
"taildrop-receive-mode-pkexec": "pkexec (prompt each time)",
38+
"authentication": "Authentication",
39+
"login-server": "Login Server",
40+
"login-server-desc": "Custom login server URL (e.g. Headscale). Leave empty for default Tailscale server."
3841
},
3942
"actions": {
4043
"widget-settings": "Widget Settings"
@@ -49,7 +52,8 @@
4952
"ssh": "SSH to host",
5053
"ping": "Ping host",
5154
"use-exit-node": "Use as Exit Node",
52-
"send-file": "Send File"
55+
"send-file": "Send File",
56+
"login": "Login"
5357
},
5458
"toast": {
5559
"title": "Tailscale",
@@ -66,12 +70,15 @@
6670
"terminal-not-configured": {
6771
"title": "Terminal Not Configured",
6872
"message": "Please set a terminal command in plugin settings"
69-
}
73+
},
74+
"login-browser-opened": "Authentication page opened in browser",
75+
"login-failed": "Login failed"
7076
},
7177
"panel": {
7278
"title": "Tailscale Network",
7379
"peers": "peers",
7480
"not-connected": "Not connected",
81+
"not-authenticated": "Not authenticated",
7582
"no-peers": "No connected peers",
7683
"admin-console": "Admin Console",
7784
"terminal-warning": {

tailscale/i18n/fr.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
"taildrop-receive-mode": "Mode de réception",
3535
"taildrop-receive-mode-desc": "Comment exécuter 'tailscale file get'. Le mode opérateur ne nécessite pas de confirmation si vous avez exécuté 'sudo tailscale set --operator $USER'.",
3636
"taildrop-receive-mode-operator": "Opérateur (sans confirmation)",
37-
"taildrop-receive-mode-pkexec": "pkexec (confirmation à chaque fois)"
37+
"taildrop-receive-mode-pkexec": "pkexec (confirmation à chaque fois)",
38+
"authentication": "Authentification",
39+
"login-server": "Serveur de connexion",
40+
"login-server-desc": "URL du serveur de connexion personnalisé (ex. Headscale). Laisser vide pour le serveur Tailscale par défaut."
3841
},
3942
"actions": {
4043
"widget-settings": "Paramètres du widget"
@@ -49,7 +52,8 @@
4952
"ssh": "SSH vers l'hôte",
5053
"ping": "Ping l'hôte",
5154
"use-exit-node": "Utiliser comme nœud de sortie",
52-
"send-file": "Envoyer un fichier"
55+
"send-file": "Envoyer un fichier",
56+
"login": "Se connecter"
5357
},
5458
"toast": {
5559
"title": "Tailscale",
@@ -66,12 +70,15 @@
6670
"terminal-not-configured": {
6771
"title": "Terminal non configuré",
6872
"message": "Veuillez définir une commande de terminal dans les paramètres du plugin"
69-
}
73+
},
74+
"login-browser-opened": "Page d'authentification ouverte dans le navigateur",
75+
"login-failed": "Échec de la connexion"
7076
},
7177
"panel": {
7278
"title": "Réseau Tailscale",
7379
"peers": "pairs",
7480
"not-connected": "Non connecté",
81+
"not-authenticated": "Non authentifié",
7582
"no-peers": "Aucun pair connecté",
7683
"admin-console": "Console d'administration",
7784
"terminal-warning": {

0 commit comments

Comments
 (0)