feat(tailscale): add Tailscale control center widget#1875
feat(tailscale): add Tailscale control center widget#1875Gdetrane wants to merge 1 commit intoAvengeMedia:masterfrom
Conversation
|
Hi there, thanks for the PR. I think this is better suited as an optional plug-in rather a native DMS feature as it's best fit for a select number of users. I will refer to @bbedward for a final accessment. |
|
@Purian23 Hi, thanks for jumping in, that's fair. I thought this could be a nice built-in feature, given Tailscale's growth and it being included in some distros, but I totally see your point. I'm still new to this repo, but I imagine this would require keeping the core Go logic and moving the QML logic to a plugin package, right? Any feedback is welcome |
|
I would 100% use that! |
|
Thanks @mecattaf I really appreciate that 😄 |
Been a ublue user for 5+ years! Specifically have my own bluebuild recipe
Actually DMS ships with the miracle window manager official distro but that's super niche. Otherwise check out Zirconium (dms + niri) |
|
Thanks again for the forward thinking here! The main dev @bbedward is out this week but we'll review whether a plugin would work or by using it natively in a few days! |
|
Just another quick note on the plugin option route I mentioned. One already exists for you to see how it works. |
|
@Purian23 No worries at all, we all need time off 👍 Just a little note on the plugin route, I think this might be a bit too complex for a plugin if I understand this correctly, given there's some Go code to handle everything via Tailscale's Unix sockets API, which imho is cleaner than relying on the cli as a middleman. I'm not sure obviously, I don't exactly have a ton of experience with implementing tools using Unix sockets from my day to day work, but my intuition was in general to use the APIs that are natively provided to integrate with whatever the underlying system is |
bbedward
left a comment
There was a problem hiding this comment.
This looks mostly good, and I think it's fine as a built-in, though its worth noting a plugin could interface with it directly since it is a unix socket API, which can be done in QML/JS
|
Hi @bbedward thank you for the review, I will happily address all the requested changes asap |
|
@Gdetrane Also consider using the tailscale GO client, it would probably simplify things quite a bit and help compatibility/maintenance long-term. https://github.com/tailscale/tailscale/blob/main/client/local/local.go |
|
@bbedward I noticed the Tailscale go client requires a very recent go version, 1.26+, considering there might be CI involved with 1.25, nix packages I haven't verified availability for, I think I will simply assess your feedback in code without changing the core implementation, but in the future maybe the tailscale go client will be the way to go (pun intended). |
|
Bumping GO version is no big deal, CI should be unaffected since it pulls version from go.mod |
|
@bbedward Cool, I will dive deeper into the library then, it doesn't seem to have a ton of dependencies either, so win win |
1099a17 to
b2da0ac
Compare
|
@bbedward moved to the Tailscale go client and refactored based on it, I made some extra modifications fixing a couple of lingering issues I've noticed in manual testing done on my desktop. I believe your points should be addressed now, but ptal :) |
Full-stack Tailscale integration for DMS control center: Backend (Go): - Tailscale manager using official tailscale.com/client/local library - Typed conversion from ipnstate.Status to QML-friendly IPC types - 3s context-based poll timeout, 30s poll interval - Manager cleanup in cleanupManagers() - Comprehensive test coverage (16 tests) Frontend (QML): - TailscaleService with WebSocket subscription - TailscaleWidget with peer list, filter chips, search - Copy-to-clipboard for IPs and DNS names - Daemon lifecycle handling (offline/stopped states) Dependencies: - Add tailscale.com v1.96.1 (official local API client) - Bump Go to 1.26.1 (required by tailscale.com) - Bump golangci-lint hook v2.9.0 -> v2.11.3
b2da0ac to
4fd16c4
Compare
|
Noticed new conflicting changes merged upstream, rebased again |
| // Eagerly try to initialize Tailscale manager; if it fails (socket not | ||
| // found), start a background retry loop so the widget recovers when | ||
| // tailscaled starts later. | ||
| if err := InitializeTailscaleManager(); err != nil { |
There was a problem hiding this comment.
This looks like it will never be triggered, because tailscale.NewManager() never returns an error.
| current := m.state | ||
| m.stateMutex.RUnlock() | ||
|
|
||
| if !reflect.DeepEqual(m.lastState, current) { |
There was a problem hiding this comment.
lastState RW should also be protected by mutex, or we could probably be more cautious with a notifier in a single goroutine pattern that might be more thread-safe, like some other managers do.
| return err | ||
| } | ||
|
|
||
| tailscaleManager = manager |
There was a problem hiding this comment.
tailScaleManager probably needs mutex protection, seems like it can be written from multiple goroutines.
| func HandleRequest(conn net.Conn, req models.Request, manager *Manager) { | ||
| switch req.Method { | ||
| case "tailscale.subscribe": | ||
| handleSubscribe(conn, req, manager) |
There was a problem hiding this comment.
We can remove this case, since the pattern is to subscribe through the main server.go with what we want. Other managers may do it (but its not a pattern thats used)
| ) | ||
|
|
||
| const ( | ||
| pollInterval = 30 * time.Second |
There was a problem hiding this comment.
I would really prefer to not do polling here, I think we can use watch-ipn-bus : https://github.com/tailscale/tailscale/blob/54606a0a89bc5e90b5295179e5b776f6a2df8cfa/ipn/localapi/localapi.go#L119 , that would be much nicer
| ) | ||
|
|
||
| const ( | ||
| pollInterval = 30 * time.Second |
There was a problem hiding this comment.
As a bonus without polling maybe we can avoid all of the reflect.DeepEqual usage. Which has a few risks:
- Very slow and inefficient
- May evaluate as not equal on fields you dont care about
| var capabilitySubscribers syncmap.Map[string, chan ServerInfo] | ||
| var cupsSubscribers syncmap.Map[string, bool] | ||
| var cupsSubscriberCount atomic.Int32 | ||
| var tailscaleSubscribers syncmap.Map[string, bool] |
There was a problem hiding this comment.
We increment these but I can't tell that they actually matter or are used for anything. Ideally we would stop the retry loop when its 0 at least
| property string magicDnsSuffix: "" | ||
| property string tailnetName: "" | ||
| property var selfNode: null | ||
| property var peers: [] |
There was a problem hiding this comment.
The way this is computed probably triggered an inefficient binding chain, but it's not a major deal for now.
|
@bbedward taking some time to address all the issues, I'm a little swamped at work right now |

Summary
Native Tailscale integration as a builtin control center plugin — search, filter,
and copy IPs or FQDNs of any node in the tailnet without opening the web admin
panel or using the CLI.
Motivation
Tailscale is a rapidly growing mesh VPN with a vibrant community, now shipping by
default in distros like Bazzite and other Fedora variants. Existing desktop
integrations (e.g., GNOME extensions) are limited and unreliable. This widget
provides quick, native access to tailnet device information directly from the DMS
control center.
Changes
Go backend (core/internal/server/tailscale/)
tailscale.com/client/local(v1.96.1) forautomatic socket discovery and typed API responses, with 3s context-based poll
timeout and 30s poll interval
ipnstate.Statusinto lean IPC types withpeer sorting (online first, alphabetical), DNS-derived hostnames (unique per
node), user ID resolution, and relative time formatting
tailscale.getStatus (one-shot), tailscale.refresh (manual poll)
tailscaled is not running at startup; polls keep running through daemon
stop/restart cycles for automatic recovery
lifecycle, poll errors, handler routing, and state comparison
QML frontend
following the CupsService pattern
online peer count) and expandable detail view
compose with search (tagged devices correctly included in My Online)
applies within the active filter
Integration
Dependencies
tailscale.comv1.96.1 (official local API client, ~22 transitive deps, pure Go)Daemon lifecycle handling
Test plan
make test)Tested on: