Skip to content

tailscale: Tailscale status & control (port of the v4 tailscale plugin) - #72

Draft
rylos wants to merge 10 commits into
noctalia-dev:mainfrom
rylos:add-tailscale
Draft

tailscale: Tailscale status & control (port of the v4 tailscale plugin)#72
rylos wants to merge 10 commits into
noctalia-dev:mainfrom
rylos:add-tailscale

Conversation

@rylos

@rylos rylos commented Jul 21, 2026

Copy link
Copy Markdown

Plugin

  • Id: rylos/tailscale
  • New plugin
  • Update to an existing plugin (version bumped in plugin.toml)

What it does

Tailscale status and control, built around fast host access: see which peers are online and copy their IP or name in
one gesture. A port of the v4 tailscale
plugin (credited in the README, MIT) to the v5 Luau plugin API.

Entries: bar widget (bar), panel (panel), headless poller service (poller), up/down control-center shortcut
(toggle), /ts launcher provider (hosts), desktop widget (desktop).

Highlights: /ts <query> fuzzy-searches the tailnet and Enter copies the peer's IP (/ts n <query> copies its
Tailscale short name); panel with online-first peer list, live filter, per-row copy IP / copy name / ping / SSH /
use-as-exit-node, last-seen for offline peers, exit-node banner, Taildrop receive. Duplicate host names are
disambiguated with the unique Tailscale short name.

Not ported from v4: Taildrop send (the plugin API has no file picker) and multi-account switching.

External dependencies

Declared in dependencies and documented in the README Requirements table:

Command Why
tailscale Status polling (tailscale status --json), up/down, set --exit-node=…, ping -c 3 <ip>, file get <dir>.
ssh The panel's per-peer SSH button (ssh <tailscale-ip>), launched in the user's terminal via noctalia.runInTerminal.
xdg-user-dir Resolves the default Taildrop folder when the taildrop_dir setting is empty.
gio Opens the daemon's auth URL when login is required.
xdg-open Fallback for that same auth URL when gio is unavailable.

Only tailscale is needed for the core flows; the rest back optional actions.

Every network call, filesystem access, and spawned process

  • Network: none from the plugin itself — everything goes through the Tailscale CLI.
  • Processes: exactly the commands in the table above, nothing else.
  • Filesystem: Taildrop receive lists the download directory before and after tailscale file get to report which
    files arrived. No other reads or writes.
  • Privacy: host names/IPs/last-seen come from the user's own tailnet via the local CLI and are only displayed or
    copied to the local clipboard.
  • Shell safety: the runtime takes a command line rather than an argv, so every interpolated value (Taildrop
    directory, peer IP, auth URL, SSH target) is single-quote shell-escaped; the auth URL is pattern-checked and
    ssh_username is rejected unless it is a plain login name.

Testing

Developed and tested live against a real 18-peer tailnet: launcher copy flows (/ts, /ts n), panel actions
(copy IP/name, ping, SSH, exit node set/clear), Taildrop receive in operator mode, host-name deduplication, every bar
state (running / stopped / needs-login / not-installed), the desktop widget, and the control-center shortcut.
noctalia plugins lint: 0 errors, 0 warnings.

  • Tested on Niri
  • Tested on Hyprland
  • Tested on Sway
  • Tested on another compositor:
  • Noctalia version tested against: 5.0.0_beta.3
  • Plugin API level: 3

Screenshots / Videos

Host names and Tailscale IPs are masked.

Bar widget (tooltip with state, this node and the peer count)

Tailscale bar widget with its tooltip

Panel (online-first peer list, filter, per-row copy / ping / SSH / exit-node buttons)

Tailscale panel

Launcher (/ts <query>, Enter copies the peer's IP)

Tailscale launcher provider

Checklist

  • The directory name matches the part of id after the / in plugin.toml exactly.
  • It ships plugin.toml, README.md, thumbnail.webp, and translations/en.json.
  • README.md follows the
    README template, documents
    every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
  • I created thumbnail.webp with the thumbnail generator.
  • version follows semver and is bumped in this PR; plugin_api is the oldest API level this plugin requires.
  • Every non-English translation in this PR uses a locale supported by Noctalia core, and I can read, write, and
    understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
    (This PR ships en.json and it.json; Italian is my native language.)
  • I did not edit catalog.toml; CI generates it.
  • This PR touches exactly one plugin directory.

Code review attestation

  • The code is readable and not obfuscated, minified, or generated.
  • It does not download and execute remote code.
  • Every network call, filesystem write, and spawned process is something the description above accounts for.
  • I have the right to publish this code under the license declared in plugin.toml.

Credits

Based on the v4 tailscale plugin, MIT.

rylos and others added 2 commits July 21, 2026 10:12
Port of the v4 tailscale plugin to the v5 Luau plugin API, built around
fast host access: /ts launcher provider (Enter copies a peer's IP,
"/ts n" copies its name), a panel with online-first peer list, per-row
copy/ping/SSH/exit-node actions and live filter, connect/disconnect,
Taildrop receive, last-seen for offline peers, up/down shortcut, and a
desktop widget. All host interaction goes through the tailscale CLI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ItsLemmy

Copy link
Copy Markdown
Contributor

• 1. blocking - tailscale/service.luau:224
Dynamic values are interpolated into shell command strings without shell-safe quoting. The configured Taildrop
directory permits command substitution even inside double quotes, and paths containing quotes break command
boundaries. Similar unsafe interpolation occurs for ssh_username at tailscale/panel.luau:419 and the
authentication URL at tailscale/service.luau:293. These values can alter the command executed by sh when the
corresponding action is invoked. Shell-quote every dynamic argument, or use an argument-vector API where
available.

  1. blocking - tailscale/plugin.toml:14
    The manifest declares only tailscale, but the plugin also executes xdg-user-dir, gio, xdg-open, and ssh at
    tailscale/service.luau:249, tailscale/service.luau:294, and tailscale/panel.luau:421. Repository policy requires
    external commands to be declared in dependencies and mentioned in the README Requirements section. Add and
    document all runtime command dependencies.

  2. Use the thumbnail generator, and use the proper PR template. you skipped all the checklist and important stuff

@ItsLemmy
ItsLemmy marked this pull request as draft July 22, 2026 03:56
Review follow-up on noctalia-dev#72:

- shell-quote every interpolated value in spawned command lines
  (Taildrop dir, ping/exit-node IP, auth URL, ssh target) with a
  single-quote helper; tighten the auth-URL check and reject an
  ssh_username that is not a plain login name.
- declare ssh, xdg-user-dir, gio and xdg-open in dependencies and
  document each one in the README Requirements section.
@rylos

rylos commented Jul 26, 2026

Copy link
Copy Markdown
Author

Thanks for the review — 1 and 2 are fixed in 9f71603, and the PR description is now filled in with the template.

1. Shell quoting. The runtime only exposes a command line (noctalia.runAsync(cmd) / noctalia.runInTerminal(cmd)), no argv API, so every dynamic value is now passed through a single-quote escaping helper (''\''):

  • service.luau — Taildrop directory in tailscale file get, peer IP in tailscale ping and tailscale set --exit-node=, auth URL in gio open / xdg-open (the URL is also pattern-checked against a strict allowed character set, not just the https?:// prefix).
  • panel.luau — the SSH target; on top of quoting, ssh_username is rejected with a toast unless it is a plain login name ([A-Za-z0-9._-]).

2. Dependencies. plugin.toml now declares ["tailscale", "ssh", "xdg-user-dir", "gio", "xdg-open"], and the README Requirements section is a table that names each one with what it is used for (and notes only tailscale is needed for the core flows).

3. Template / thumbnail. The description now follows the PR template with the checklist filled in. Screenshots and a regenerated thumbnail.webp from the thumbnail generator are coming in a follow-up push — I left that checklist item unticked until then.

CI is green on the current head.

rylos added 6 commits July 26, 2026 23:32
The short name (and the MagicDNS FQDN) only resolve when MagicDNS is
enabled, so the SSH button opened a terminal that exited immediately
with an unresolved-hostname error. The Tailscale IP always works.
`tailscale ping` exits non-zero and prints "direct connection not
established" when the peer answers only through a DERP relay, so a
perfectly reachable host was reported as a ping failure. Report the last
pong line and note that the connection is relayed.
noctalia.formatTime takes strftime patterns, so "hh:mm:ss" was echoed
back verbatim and the panel footer read "Last check: hh:mm:ss".
A launcher `badge` is drawn in the icon slot and scaled to fill it, so
"online"/"offline" covered half the row and hid the OS glyph. Put the
state at the front of the subtitle instead.
@rylos

rylos commented Jul 26, 2026

Copy link
Copy Markdown
Author

Point 3 is done too: the description now has screenshots (bar widget with tooltip, panel, /ts launcher — host names and Tailscale IPs masked) and thumbnail.webp was regenerated with the official generator.

While taking those screenshots I also found and fixed three real bugs:

  • The SSH button targeted the peer's Tailscale short name, which only resolves when MagicDNS is enabled — it now uses the Tailscale IP (7bb0f94).
  • tailscale ping exits non-zero and prints direct connection not established when the peer answers through a DERP relay, so a perfectly reachable host was reported as a ping failure (2f5876e).
  • noctalia.formatTime takes strftime patterns, so the panel footer literally read Last check: hh:mm:ss (7da0470).
  • A launcher badge is drawn in the icon slot and scaled to fill it, so online/offline covered half the row and hid the OS glyph; the state moved to the subtitle (0446502).

The plugin also ships an Italian translation now (c81ad73) — Italian is my native language, so I can maintain it.

CI is green and noctalia plugins lint reports 0 errors, 0 warnings on the current head. Ready for another look.

The root column's height is what sizes the panel; keeping it as a named
constant next to a note about plugin.toml stops the two from drifting.
@ItsLemmy

Copy link
Copy Markdown
Contributor

The panel uses 24 predeclared callback slots and silently truncates the peer list at tailscale/panel.luau:255. The current plugin UI API supports closures directly in callback properties, with handlers replaced on each render. Capture each row's peer in its button callbacks instead, then remove MAX_SLOTS, the slot table, and the numbered callback globals. This removes the arbitrary peer limit and substantially simplifies the panel.

Recommend removing:

  • MAX_SLOTS
  • slots
  • ipAt, nameAt, pingAt, sshAt, and exitAt
  • All numbered onIp*, onName*, onPing*, onSsh*, and onExit* globals
  • The silent 24-peer cutoff

Each row can capture its peer directly:

  ui.button({                                                                                              
    glyph = "activity",                                                                                    
    onClick = function()                                                                                   
      sendCmd("ping", { ip = peer.ip, name = peer.name })                                                  
    end,                                                                                                   
  })                                                                                                       

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants