Tray state icon and window picture; firewall rules without the comment match; macOS app bundle

The tray icon is dark red with no connection (or a lost exit node), blue when
connected and green while an exit node is in use; the windows get the logo and
an app id that matches the desktop entry, and the exit icon moves to the
bottom left of the tile. Exit-node and broadcast firewall rules fall back to
being recognised by their shape on kernels that have no `comment` match, as
small router builds do. The macOS archive carries Tsunagi.app for the Homebrew
cask, and the daemon plist opens the control socket to the admin group.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aleksandr BogomiakovandClaude committed 2026-10-03 03:30:47 +01:00
1 parent c9c653023d
commit 0cfc1cbafc
14 files changed
+692 -90

No files matched your search

+1
View File
@@ -9,3 +9,4 @@ Terminal=false
Categories=Network;Utility;
Keywords=mesh;vpn;overlay;network;
StartupNotify=false
StartupWMClass=tsunagi-tray
+14
View File
@@ -27,6 +27,20 @@ shell started `sudo`:
| disposable cache | `/var/db/tsunagi/cache` |
| control socket | `/var/run/tsunagi/agent.sock` |
## Homebrew
```sh
brew tap house-of-vanity/tap
brew install --cask tsunagi-gui # the tray app, and the agent it needs
# or just the command line agent:
brew install tsunagi
sudo brew services start tsunagi # the agent, as a root daemon at boot
```
The cask starts the service for you. The agent runs as root, and its control
socket is open to the `admin` group, so the CLI and the tray work without
`sudo` for an administrator.
## Native deployment — root LaunchDaemon
For an always-on agent that starts at boot, install the bundled
Vendored Executable
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Wrap the tray GUI in a macOS application bundle, so it can be installed as an
# app (a Homebrew cask, or dragging it to /Applications) instead of being a bare
# executable.
#
# Usage: build-app.sh <tray_binary> <version> <output.app>
# <tray_binary> the built tsunagi-tray executable
# <version> the release version (the tag without its leading v)
# <output.app> the bundle to create; replaced if it exists
#
# It is a menu-bar app: LSUIElement keeps it out of the Dock and the app
# switcher, and the tray starts the window as a second process of the same
# executable, which inherits that. The bundle is signed ad hoc where `codesign`
# exists (the macOS runner), because arm64 will not run unsigned code; it is not
# notarised, so a downloaded copy needs its quarantine flag cleared, which the
# cask does.
set -euo pipefail
tray="$1"
version="$2"
app="$3"
# CFBundleShortVersionString is dotted numbers only; 0.1.0-rc.22 becomes 0.1.0.
short="${version%%-*}"
rm -rf "$app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
install -m 0755 "$tray" "$app/Contents/MacOS/tsunagi-tray"
cat > "$app/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Tsunagi</string>
<key>CFBundleDisplayName</key>
<string>Tsunagi</string>
<key>CFBundleIdentifier</key>
<string>cy.hexor.tsunagi.tray</string>
<key>CFBundleExecutable</key>
<string>tsunagi-tray</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>${short}</string>
<key>CFBundleShortVersionString</key>
<string>${short}</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>LSUIElement</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
PLIST
if command -v codesign >/dev/null 2>&1; then
codesign --force --deep --sign - "$app"
fi
echo "built $app"
+9
View File
@@ -36,6 +36,15 @@
<key>ThrottleInterval</key>
<integer>10</integer>
<!-- The control socket belongs to the admin group, so the CLI and the tray
work without sudo for any administrator. A plain account is not in it
and cannot manage the agent. -->
<key>EnvironmentVariables</key>
<dict>
<key>TSUNAGI_CONTROL_GROUP</key>
<string>admin</string>
</dict>
<!-- Logs. launchd creates these as root; rotate them with newsyslog if you
keep the daemon around. -->
<key>StandardOutPath</key>