macOS app
claimward-vpn-app-osx
v0.2.0 is a menu-bar (tray) app written in Go whose whole user interface
is a Svelte single-page app rendered in a webview. Its logic (sign-in,
tenant choice, connect) and its helper are the shared
pkg/appcore and pkg/helper.
Design
claimward-app (tray process, as you)
├─ systray menu: status / Open Claimward… / Configuration… / Connect / Disconnect / Quit
├─ uiserver loopback HTTP: embedded Svelte SPA + token-guarded JSON API
└─ appcore sign-in, tenant choice, session; drives the helper
│ spawns "claimward-app ui <url>" │ Unix socket (JSON), 0660 root:admin
▼ ▼
webview (WKWebView) claimward-helper (root LaunchDaemon)
renders the Svelte UI enrolls with the server, wireguard-go: utunN,
route pushesThe tray process owns all state and serves both the UI and a small JSON API on
127.0.0.1, guarded by a per-launch token compared in constant time. The
webview is a thin window pointed at that loopback URL, in a separate process
because only one Cocoa run loop can own the main thread. The
helper enrolls with the server and
owns the tunnel; the app is unprivileged.
Build
With go-task:
task config:init # a starter config.json (local Dex)
task install-helper SERVER=https://vpn.example.org # build and install the helper (sudo)
task start:bundle # build Claimward.app and open itRun the bundle rather than the bare binary: a bare binary started from a menu-bar agent cannot bring its window to the front. By hand:
cd frontend && npm install && npm run build && cd .. # the Svelte UI, embedded with go:embed
CGO_ENABLED=1 go build -o bin/claimward-app ./cmd/claimward-app
CGO_ENABLED=0 go build -o bin/claimward-helper ./cmd/claimward-helperConfigure
~/Library/Application Support/Claimward/config.json, also editable from
Configuration… (GitHub is the default):
{
"server_url": "https://vpn.example.com",
"provider": "github",
"github_client_id": "Iv1.0123456789abcdef"
}For OIDC set "provider": "oidc", for go-authn "provider": "go-authn", with
"oidc_issuer" and "oidc_client_id". With GitHub and go-authn, clicking
Connect shows a code to enter at the displayed page; with OIDC the browser
opens the provider’s sign-in page.
Install the helper and run
sudo ./scripts/install-helper.sh https://vpn.example.org # more servers may follow
open dist/Claimward.app # then click ConnectThe installer copies the helper to /Library/PrivilegedHelperTools/, writes
/Library/Application Support/Claimward/helper.json (root’s, mode 0644)
with the servers given, and loads the LaunchDaemon com.claimward.helper.
The helper’s socket is /var/run/claimward-helper.sock, 0660,
root:admin; it logs to /var/log/claimward-helper.log.
sudo ./scripts/uninstall-helper.sh removes it.
Tenants
A person in several tenants chooses one in the window (“Choose a tenant…”), which offers them once the server says there is a choice, or on request. The choice holds for the session; a new sign-in forgets it. A Connect from the menu bar that the server refuses for want of a choice opens the window.
Release
.github/workflows/release.yml builds Claimward.dmg on v* tags and
attaches it to the release. When the signing secrets are set it signs the app
with a Developer ID and notarizes and staples the DMG; without them it falls
back to an ad-hoc signature, which Gatekeeper blocks on a downloaded DMG. The
repository holds no signing secrets, so the published DMGs (v0.1.0 and
v0.2.0) are ad-hoc signed and not notarized. The DMG holds the app (with the helper binary inside it); the
helper is installed as above.
0600 file
today), the helper installed with SMJobBless, a release signed with a
Developer ID, and DNS applied on macOS.Created on by tannevaled
Modified on by tannevaled