Omahub
← All plugins
J

Omanodes

by jwhall

Join, leave and inspect ZeroTier networks from the Omarchy bar. Unofficial third-party widget for ZeroTier networks; not affiliated with ZeroTier, Inc.

Security review

Potentially dangerous behavior detected · 13 findings

Deterministic scan — not a security guarantee

High
Risk level
High
Analyzed commit
18e88e9
Scanned
1 month ago
  • high destructive_filesystem tests/test-safety.sh:25

    Destructive operation on the root filesystem or a block device.

    rm -rf /"; do
  • medium sudo backend.sh:19

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo branches exec SYSTEM_BACKEND, a root-owned copy
  • medium sudo backend.sh:26

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo normally provides, and if that rule names the
  • medium sudo backend.sh:98

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo exposes
  • medium sudo backend.sh:121

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo and pkexec both set it, but a bare-root invocation could
  • Augments a command with octal/hex escape sequences.

    \x02D\x01\x00;")
  • Docs sudo README.md:38

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo install -Dm755 backend.sh /usr/lib/omanodes/backend.sh
  • Docs sudo README.md:39

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo install -Dm644 polkit/org.jwhall.omanodes.policy \
  • Docs sudo README.md:72

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo has no counted-repeat form. `zerotier-cli`
  • Docs sudo README.md:239

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo rm -f /usr/share/polkit-1/actions/org.jwhall.omanodes.policy
  • Docs sudo README.md:240

    Command runs with sudo, elevating the process beyond the plugin environment.

    sudo rm -f /usr/lib/omanodes/backend.sh
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo systemctl stop zerotier-one`) and confirm the panel surfaces a
  • Command runs with sudo, elevating the process beyond the plugin environment.

    sudo path vs pkexec path.** `require_root()` picks `sudo` when stdin is

Automated analysis only — not a security guarantee.

AI advisory review

No obvious issues detected

Language-model assessment · ~deepseek/deepseek-v4-flash-latest — advisory only

Low
AI risk level
Low
Recommendation
install
Model
~deepseek/deepseek-v4-flash-latest
Analyzed commit
18e88e9
Reviewed
1 month ago

The plugin is a well-engineered ZeroTier widget that requires root to invoke zerotier-cli, self-elevating via pkexec/sudo with a documented polkit policy. The deterministic scan's high-risk flags are false positives: the `rm -rf /` appears only as a test input string in a validation test, and the hex escapes are part of a test probe's GIF payload. The actual code is security-conscious (input validation, PATH pinning, symlink-safe locking, rich-text sanitization) and performs no destructive or hidden actions.

  • Requires a root-owned backend script and a polkit policy granting passwordless execution to active sessions; misconfiguration (e.g., pointing the policy at a user-writable path) could create a privilege escalation vector, but the README explicitly warns against this and the shipped policy targets /usr/lib/omanodes/backend.sh.
  • The plugin relies on the user manually installing the backend and policy; if a user installs a tampered copy, the plugin would run it as root, but that is outside the plugin's control.
  • The polkit allow_active=yes grants any active local session the ability to run the backend without a password, which is a minor trust boundary but consistent with similar network-management tools.
How this check works

This review combines the deterministic scan (the rule-based results above) with an independent look at the plugin's code by a language model. The model reads a trimmed sample of the repository's files, the manifest, and the README, then gives a plain-language risk level and a recommendation: install (no notable danger), review (look closer first), or avoid (clearly dangerous).

It runs on the same analyzed commit as the deterministic scan and is strictly advisory — it is not a security guarantee and never blocks a plugin by itself. A human moderator still reviews plugins before they are listed.

AI advisory only — automated analysis, not a security guarantee.

Install
$ omarchy plugin add https://github.com/jwhall/omarchy-omanodes --enable
System #bar #quickshell #system

Omanodes — ZeroTier networks in the Omarchy bar

List, join, leave and inspect ZeroTier networks from the Omarchy bar.

This is an unofficial third-party widget. It is not affiliated with, endorsed by, or connected to ZeroTier, Inc.

Everything goes through zerotier-cli, the same tool the zerotier-one service ships. This plugin does not talk to the ZeroTier Central API or any network controller directly — it only reflects what your local node knows about its own memberships.

Requirements

  • zerotier-one — installed and running (systemctl status zerotier-one).
  • polkit — for privilege escalation (see below). Present by default on Omarchy/most desktop distros.

Why this needs root, and what that means for you

zerotier-cli authorizes itself against the local zerotier-one service by reading /var/lib/zerotier-one/authtoken.secret, which is mode 0600, owned by the zerotier-one system user. Only root can read it. That means every call this plugin makes — including the read-only status poll that drives the panel, not just joining or leaving — needs root.

backend.sh self-elevates via pkexec (or sudo in a terminal), the same pattern Omarchy's own omarchy-dns uses. Without more configuration this pops a polkit authentication dialog on every call — and the panel polls every refreshIntervalSec (10s by default), so you must install the bundled polkit policy below, or the widget will be unusable (a password prompt every few seconds).

Install the polkit policy and backend (required)

sudo install -Dm755 backend.sh /usr/lib/omanodes/backend.sh
sudo install -Dm644 polkit/org.jwhall.omanodes.policy \
  /usr/share/polkit-1/actions/org.jwhall.omanodes.policy

The policy grants allow_active=yes: an active local graphical session runs the backend without a password, the same trust level NetworkManager's own polkit rules give an active session for network control. That trust level covers physical presence at the session, not administrative privilege — so the script it authorizes must live somewhere that active-but-unprivileged user can't write to. That's why the policy points at /usr/lib/omanodes/backend.sh (root-owned, installed by the command above) rather than at this plugin's own checkout under ~/.config/omarchy/plugins/, which the same user owns. It does not grant passwordless root generally — only for that one root-owned script.

If you update the plugin (which changes backend.sh), re-run the install -Dm755 backend.sh ... step to refresh the system copy — the policy's exemption always runs whatever is currently installed at /usr/lib/omanodes/backend.sh, not the plugin checkout's copy.

If you'd rather not install a system-wide polkit action, the alternative is a sudo NOPASSWD rule in /etc/sudoers.d/ (visudo -f /etc/sudoers.d/omanodes), scoped to the installed root-owned backend and to the exact three commands it accepts:

Cmnd_Alias OMANODES = /usr/lib/omanodes/backend.sh status, \
                      /usr/lib/omanodes/backend.sh join [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f], \
                      /usr/lib/omanodes/backend.sh leave [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
youruser ALL=(root) NOPASSWD: OMANODES

(The 16 repeated character classes are sudo's glob syntax for "exactly 16 lowercase hex digits" — sudo has no counted-repeat form. zerotier-cli prints network IDs in lowercase; add A-F to the classes if you type them uppercase.)

Do not write the rule as NOPASSWD: /usr/bin/zerotier-cli (or any unbounded command). Passwordless zerotier-cli with no argument bound hands the account its entire root command surface — zerotier-cli set ... and friends reconfigure the node, and an unbounded rule is a standing passwordless-root primitive rather than the three operations this widget needs. Likewise, never point the rule at backend.sh inside the plugin checkout: NOPASSWD removes the password boundary that normally makes a user-writable target safe under sudo, so it must name the root-owned /usr/lib/omanodes/backend.sh — which is also what require_root() execs.

Note that backend.sh only takes the sudo branch when stdin is a TTY, which the panel's background Process invocations are not, so this path only helps if you also adjust require_root() or invoke the backend from a terminal yourself.

Install

omarchy plugin add https://github.com/jwhall/omarchy-omanodes.git
omarchy plugin enable jwhall.omanodes right

Then install the polkit policy as described above — the plugin will load without it, but the panel will be unusable until you do.

Using it

In the bar: left click opens and closes the panel, middle click refreshes.

In the panel:

Key Action
j / k, arrows move the cursor
Enter leave the selected network (asks first), or open the join prompt from the header
n join a network by ID
l leave the selected network (asks first)
i show network info
r refresh
Esc close

Hovering (or moving the keyboard cursor to) a network row reveals two buttons: a leave/join toggle and an info button that opens the network's detail window.

Joining and leaving are not "connect/disconnect." ZeroTier has no concept of temporarily pausing a network while staying a member — zerotier-cli only supports join and leave. Leaving a PRIVATE network removes your authorization on its controller; rejoining may require the controller to re-approve you (instant if it remembers you, not guaranteed otherwise). The row button and the confirmation dialog say "Leave", not "Disconnect", on purpose.

Network info shows what this host's local ZeroTier service knows about one network: name, ID, type, status, this host's assigned address(es), MAC, and the local interface name. It does not show other members of the network. zerotier-cli on a regular member node has no way to list a network's other members or their assigned addresses — that data lives only on the network's controller (ZeroTier Central, or a self-hosted controller's own API), which this plugin does not talk to.

Settings

Key Default Range
refreshIntervalSec 10 2–3600
omarchy bar set jwhall.omanodes refreshIntervalSec 30

Raising this reduces both polkit prompt frequency (if you haven't installed the policy) and the load of a background elevated call running constantly — but it also means the panel's list is stale for longer after a change made outside the widget (zerotier-cli join ... from a terminal, another host approving your membership, etc.).

IPC

omarchy-shell jwhall.omanodes open      # also: close, show, hide
omarchy-shell jwhall.omanodes refresh
omarchy-shell jwhall.omanodes status    # "N network(s) joined"
omarchy-shell jwhall.omanodes join 93afae5963b868fd
omarchy-shell jwhall.omanodes leave 93afae5963b868fd

What it touches

  • zerotier-cli -j listnetworks / join / leave — every call self-elevates to root via pkexec/sudo; see above.
  • $XDG_RUNTIME_DIR/omarchy-omanodes.<uid>.lock.d — a per-user flock so concurrent join/leave clicks (one widget instance per monitor) serialize instead of racing. Private, gone at reboot.
  • No files outside of that lock; no state is persisted between sessions — network membership itself is zerotier-one's own state, not this plugin's.

Tests

tests/ runs backend.sh against a fake zerotier-cli on PATH, checks the QML side against controller-supplied text (see below), and keeps a manual checklist (tests/checklist.md) for what needs a real zerotier-one service and a real polkit prompt:

bash tests/run.sh

tests/test-qml-markup.sh needs Qt's qml runtime (qt6-declarative) for its runtime half and skips that part if it is missing; the static half always runs.

Untrusted text from the controller

A network's name is set on the ZeroTier controller, not on this host, so it is untrusted input to the widget. QML's default textFormat: Text.AutoText sends anything Qt recognises as markup to the rich-text engine, which resolves <img src> and <a href> URLs from inside the shell process — so a crafted name could make simply opening the panel fetch a remote URL or read a local file.

Two layers close that off:

  • every Text element in this plugin pins textFormat: Text.PlainText, and
  • Service.plain() neutralises </> and control characters at the single point where controller data (and backend stderr) enters the model, which also covers the shared qs.Ui components this widget hands text to (PanelToolTip, ConfirmDialog) — those live outside this repo and still render AutoText.

tests/richtext-probe.py proves both in a real Qt scene against a loopback HTTP server: the unmitigated AutoText case must fetch the beacon (otherwise the probe is not measuring anything), while the PlainText case and plain()'s output must not.

A second, lower-severity class is display spoofing rather than URL loading. A name can carry Unicode that changes how it reads:

  • Explicit bidi controls (U+202A–U+202E, U+2066–U+2069) reorder the rendered text. Service.plain() drops them. Legitimate right-to-left names need no explicit override — they render from the characters' own inherent directionality under UAX #9 — so this costs nothing in fidelity. The directional marks U+200E/U+200F/U+061C and the joiners U+200C/U+200D are deliberately kept; stripping the joiners would break Persian, Indic scripts and emoji sequences.
  • Confusables and zero-width characters (UTS #39) can make one network's name render like another's. No string sanitising fixes this in general, since banning non-Latin scripts is not an option. It is answered structurally instead: Panel.leaveMessage() puts the nwid in the leave confirmation, so the widget's one destructive action is anchored to an identifier the controller cannot forge (the nwid is formatted by the local zerotier-one daemon, not supplied as free-form text). The name is length-bounded there so it cannot wrap the nwid off the dialog.

Neither of these is a URL-load or code-execution issue.

Uninstall

omarchy plugin remove jwhall.omanodes
sudo rm -f /usr/share/polkit-1/actions/org.jwhall.omanodes.policy
sudo rm -f /usr/lib/omanodes/backend.sh

Your ZeroTier network memberships are unaffected — they belong to zerotier-one, not this widget. Leave a network with zerotier-cli leave <nwid> if you want it gone too.

License

MIT — see LICENSE.

Trademarks

"ZeroTier" and the ZeroTier logo are trademarks of ZeroTier, Inc. This plugin's bundled icon (assets/zerotier-logo.png) is ZeroTier's mark, used solely to identify the service this widget controls. This widget is an independent third-party tool: it is neither affiliated with nor endorsed by ZeroTier, Inc. The MIT licence above covers this widget's own code and grants no rights in ZeroTier's trademarks or logo — do not redistribute the icon outside this widget's stated purpose without ZeroTier, Inc.'s permission.