Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 136 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,141 @@
# Zerotier KDE Widget
## This Widget Shows Zerotier Network Members
#### I had to stop using Linux due to school and work. yes dual boot etc. I could continue to use Linux, but I don't have time to deal with it and using Linux is time-consuming and I don't have the time to spare for it right now. That's why I can't continue developing it, I want to write it from scratch in the future. because this code is really shitty. The more I look at the code, the more I want to delete it, so I can't continue developing it right now.
#### 28/02/2024
## Features (V1.1)
- Show online member count on icon
- Click to copy zerotier ip
- Updates members status on intervals
- Multiple network selection window
- Only show online members option

## TODO (V2.0)
- [ ] Sorting system [WIP]
- [x] Select all networks
- [x] Context menu on icon
- [ ] Content auto windows height
- [ ] Change emojis to png (emoji system on kde is suck)
- [ ] Network selector theme fix
- [ ] Update interval 30s,1min+++
- [ ] Sometimes after reboot have duplicate list (idk why but it gets fixed when updates the list) (BUG)

## TODO (v3.0)
- [ ] Managing (create-update network, member operations,etc,all available API features)
- [ ] Network specific icon color
- [ ] Notification (online-offline,copy)
# Zerotier KDE Widget

A KDE Plasma 6 widget that shows the Zerotier networks this machine joined and
the peers it is connected to.

## How it gets its data

The widget talks to the **local `zerotier-one` service** on
`http://localhost:9993`, not to Zerotier Central. Central's API needs an account
token that is not available on the free plan, while the local service is part of
the daemon you already run.

That trade is worth knowing about:

| | Zerotier Central API | Local service API (used here) |
|---|---|---|
| Cost | Paid plan | Free, no account involved |
| Scope | Every member of a network, from anywhere | What this machine sees |
| Member names | Yes, from Central | No -- node ids, plus your own labels |
| Member addresses | Yes, from the controller | Recovered from the neighbour table |

So the widget lists your joined networks and the peers currently reachable, not
the full membership roster of a network.

## Setup

The daemon's auth token is only readable by root. Give your user account a copy:

```bash
sudo install -m 600 -o $USER \
/var/lib/zerotier-one/authtoken.secret ~/.zerotierOneAuthToken
```

The widget finds that file on its own -- there is nothing to paste into the
settings. If you keep the token somewhere else, paste it into the Auth token
field in the widget's settings instead.

## Features

- Connected peer count on the panel icon, as a theme-coloured symbolic icon
- Joined networks with their status and your assigned address
- Peers with their address on the virtual network, latency, and whether the
link is direct or relayed
- Click any row to copy the address you would actually connect to
- Your own labels for peers, since the local service only knows node ids
- Filter down to specific networks, or hide peers that are not reachable
- Refresh interval from 2 seconds upwards, plus a Refresh action in the
context menu

### How a peer's address is worked out

The address to connect to is the one on the virtual network -- `10.x.y.z`, not
the peer's public IP, which is only how the packets happen to travel today.

The local service does not know it: which address a network handed to which
member is the controller's business. The kernel does know, once a peer has been
talked to, and Zerotier derives a member's MAC deterministically from the
network id and the node id:

```
MAC[0] = (last byte of nwid & 0xfe) | 0x02
MAC[i] = node[i-1] XOR nwid[7-i] for i = 1..5
```

So the widget computes the MAC each peer must have, then looks it up in the
neighbour table of the network's interface. A peer that has never exchanged IP
traffic has no entry yet and shows up without an address until something -- a
ping is enough -- puts it there.

## Installation
There are three ways to install this widget in your KDE Plasma.

1. Head over to the Plasma Add-On installer by going to: `Right click on Desktop,Dock or Panel -> Add Widgets -> Get New Widgets -> Search "Zerotier KDE Widget" and Install`.
2. Download the `zerotier.plasmoid` file shared in this repo's [release section](https://github.com/Duoslow/zerotierIndicator/releases/latest) or from the widget's KDE Store [link](https://store.kde.org/p/1666827). After this, you can just do this: `Right Click on Desktop -> Add Widgets -> Install from local file -> Point to the downloaded zerotier.plasmoid file`.
3. Download the `zerotier.plasmoid` file shared in this repo's [release section](https://github.com/Duoslow/zerotierIndicator/releases/latest) and run this `
kpackagetool5 -t Plasma/Applet --install zerotier.plasmoid `
1. From the KDE Store: `Right click on the desktop, dock or panel -> Add
Widgets -> Get New Widgets -> search for "Zerotier Indicator"`.
2. From the `zerotier.plasmoid` file in this repo's
[releases](https://github.com/Hafikan/zerotierIndicator/releases/latest):
`Right click on the desktop -> Add Widgets -> Install from local file`.
3. From a checkout:

```bash
./build.sh
kpackagetool6 -t Plasma/Applet --install zerotier.plasmoid # or --upgrade
```

## Development

### Layout

| File | Role |
|---|---|
| `ui/main.qml` | The applet: panel icon, popup, refresh timers |
| `ui/zerotier.js` | Service API calls, online rules, MAC derivation |
| `ui/AuthToken.qml` | Finds the auth token |
| `ui/ShellCommand.qml` | Runs a command and returns its stdout |
| `ui/ConfigGeneral.qml` | Settings: interval, networks, token |
| `ui/ConfigPeers.qml` | Settings: peer labels |

### Two things that will bite you

**Qt 6 refuses to read local files over `XMLHttpRequest`** unless the process
was started with `QML_XHR_ALLOW_FILE_READ=1`, and it fails *silently* -- no
error, no `onerror`, the request simply never completes. That is why the auth
token is read through the executable data engine instead. The same trap applies
to anything else you might want to read off disk from QML.

**`console.log` output can be swallowed** depending on how the applet is
launched, which makes the above even harder to spot. Exercising a code path
under `qml6` and signalling results through `Qt.exit(code)` is a reliable way to
debug it:

```bash
QT_QPA_PLATFORM=offscreen qml6 test.qml; echo "exit=$?"
```

### Building

`./build.sh` packs `package/` into `zerotier.plasmoid`. To try a change without
touching the panel:

```bash
plasmawindowed org.github.hafikan.zerotierIndicator
```

Publishing an update: bump `Version` in `package/metadata.json`, run
`./build.sh`, and upload the new `zerotier.plasmoid` to the store listing. The
store compares that version string to decide who gets an update notification.

## Version history

- **v2.0** -- Ported to Plasma 6, rebuilt on the local service API, new flat UI
- **v1.1** -- Zerotier Central API, Plasma 5

## Credits

Originally written by [Duoslow](https://github.com/Duoslow/zerotierIndicator)
(Uğur Yavaş) for Plasma 5 and the Zerotier Central API. This fork carries the
Plasma 6 port and the move to the local service API, and is published under a
separate plugin id so both can be installed side by side.

## Widget GUI and Indicator
![ex](https://i.imgur.com/MYQDika.png)![a](https://i.imgur.com/y92VmYu.png)
## License

## Settings
![settings](https://i.imgur.com/Owxf7E2.png)
MIT -- see [LICENSE](LICENSE). The original copyright notice is retained.
26 changes: 23 additions & 3 deletions build.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
#!/bin/bash
cd package
zip -r ../zerotier.plasmoid *
cd ..
set -e
cd "$(dirname "$0")"

# Rebuilt from scratch: zip -r would otherwise merge into the existing archive
# and keep files that no longer exist in package/.
rm -f zerotier.plasmoid

if command -v zip >/dev/null 2>&1; then
(cd package && zip -qr ../zerotier.plasmoid .)
else
# zip is not installed everywhere; python ships a zip writer.
python3 -c '
import os, zipfile
with zipfile.ZipFile("zerotier.plasmoid", "w", zipfile.ZIP_DEFLATED) as archive:
for folder, _, files in os.walk("package"):
for name in files:
path = os.path.join(folder, name)
archive.write(path, os.path.relpath(path, "package"))
'
fi

echo "Built zerotier.plasmoid"
echo "Install with: kpackagetool6 -t Plasma/Applet --install zerotier.plasmoid"
21 changes: 13 additions & 8 deletions package/contents/config/config.qml
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import QtQuick 2.0
import org.kde.plasma.configuration 2.0
import QtQuick
import org.kde.plasma.configuration

ConfigModel {
ConfigCategory {
name: "General"
icon: "configure"
source: "ConfigGeneral.qml"
}
}
ConfigCategory {
name: "General"
icon: "configure"
source: "ConfigGeneral.qml"
}
ConfigCategory {
name: "Peer Names"
icon: "tag"
source: "ConfigPeers.qml"
}
}
20 changes: 14 additions & 6 deletions package/contents/config/main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0 http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
<kcfgfile name=""/>
<group name="General">
<entry name="updateInterval" type="Int">
<default>1</default>
<min>1</min>
<max>999</max>
<!-- The local service API is free and instant, so polling can be far
more frequent than the old Central API allowed. -->
<entry name="refreshSeconds" type="Int">
<default>10</default>
<min>2</min>
<max>3600</max>
</entry>
<entry name="zerotierToken" type="String">
<!-- Optional: only needed when the token file cannot be read. -->
<entry name="authToken" type="String">
<default></default>
</entry>
<!-- Comma separated network ids; empty means every joined network. -->
<entry name="selectednetwork" type="String">
<default></default>
</entry>
<entry name="show" type="Bool">
<entry name="showOnlyConnected" type="Bool">
<default>false</default>
</entry>
<!-- JSON object mapping a peer's node id to a user supplied label. -->
<entry name="peerNames" type="String">
<default>{}</default>
</entry>
</group>
</kcfg>
10 changes: 10 additions & 0 deletions package/contents/images/zerotier-symbolic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions package/contents/ui/AuthToken.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import QtQuick
import org.kde.plasma.plasma5support as P5Support

/**
* Finds the zerotier-one auth token.
*
* Qt 6 refuses to read local files over XMLHttpRequest unless the process was
* started with QML_XHR_ALLOW_FILE_READ=1 -- and it fails silently, the request
* simply never completes. We cannot set that for plasmashell, so the file is
* read through the executable data engine instead.
*/
Item {
id: reader

/** An explicitly configured token wins over anything on disk. */
property string configuredToken: ""

readonly property alias token: internal.token
readonly property alias source: internal.source
readonly property alias resolved: internal.resolved

// The daemon's own file is root only, so the per-user copy is normally the
// one that works. Echoing the path first tells the UI where it came from.
readonly property string command:
'for f in "$HOME/.zerotierOneAuthToken" /var/lib/zerotier-one/authtoken.secret; do'
+ ' [ -r "$f" ] && { echo "$f"; cat "$f"; break; }; done'

function resolve() {
if (configuredToken.length > 0) {
internal.token = configuredToken;
internal.source = "widget settings";
internal.resolved = true;
return;
}
internal.resolved = false;
executable.connectSource(command);
}

QtObject {
id: internal
property string token: ""
property string source: ""
property bool resolved: false
}

P5Support.DataSource {
id: executable
engine: "executable"
connectedSources: []

onNewData: function (sourceName, data) {
disconnectSource(sourceName);

const lines = String(data["stdout"] || "").split("\n").filter(function (line) {
return line.trim().length > 0;
});

internal.source = lines.length > 0 ? lines[0].trim() : "";
internal.token = lines.length > 1 ? lines[1].trim() : "";
internal.resolved = true;
}
}

Component.onCompleted: resolve()
onConfiguredTokenChanged: resolve()
}
Loading