A WireGuard endpoint switcher for Linux: a privileged daemon that owns the tunnel interface, a desktop GUI that walks you through setup, and a CLI for everything the GUI can do.
There is no config file to edit. Each "server" is a complete WireGuard tunnel —
its own private key, address, DNS, and peer — mirroring a wg-quick .conf. On first
launch you import a .conf (what providers like Mullvad/Proton hand out) or add
a self-hosted one by hand, in which case the daemon generates the keypair and shows
you the public key to register. You add servers on demand; the daemon persists
everything itself.
┌─────────────┐ Unix socket ┌──────────────────────┐
│ GUI (Tauri)│ ─── /run/wirefinder/ ───▶ │ wirefinderd (root) │
│ CLI │ wirefinderd.sock │ owns wg0, the keys, │
└─────────────┘ one JSON request/response │ and /var/lib state │
└──────────────────────┘
Four crates plus the GUI, in a Cargo workspace (Cargo.toml):
| Path | What it is |
|---|---|
proto/ |
The IPC contract: Request/Response + shared types. serde-only, no deps. |
daemon/ |
wirefinderd — the privileged daemon. Split into focused modules (below). |
cli/ |
wirefinder — the unprivileged client. Speaks only the protocol. |
ui/ |
The Tauri + React desktop GUI (its own build; excluded from the workspace). |
install/ |
systemd unit, desktop launcher, maintainer scripts. |
release/ |
Packaging helper (cargo deb). |
keys— WireGuard key generation/derivation (wraps the crypto crate).config— the persisted state (/var/lib/wirefinder/state.json, written atomically at0600since every server holds a private key) and its accessors.wgconf— a small, tested wg-quick.confparser (no INI dependency).wireguard— the kernel boundary. All netlink I/O lives behind theWireguardtrait;KernelWireguardis the real implementation. Pure validation/parsing helpers sit alongside it.daemon— the state machine: maps each request to a response and owns the connection-state policy. Generic overWireguard, so it is unit-tested against an in-memory fake — no root, no kernel, no network.server— the Unix-socket transport (framing, accept loop, locked-down socket permissions, per-connection timeouts and request size cap).main— wiring + the signal handler. Stopping the daemon is control-plane only: WireGuard runs in-kernel, so the tunnel (and its kill-switch routing) deliberately survives stops and package-upgrade restarts. Teardown happens only via an explicit disconnect, or on package removal (install/prerm).
The daemon is the sole owner of cryptographic material. Each tunnel's private key
is generated daemon-side (or imported once) and never appears in any response —
clients only ever learn the derived public key. All keys live in the same 0600
state file. The control socket lives in a 0750 root:wirefinder directory and is
itself 0660 root:wirefinder; group membership is how the unprivileged GUI is
allowed to talk to a root daemon.
Membership in the wirefinder group is therefore full tunnel control: anyone
in it can add a server pointing at an endpoint (and DNS) they choose and switch
all traffic to it. Add only the humans who should be able to reconfigure the VPN —
it is an admin boundary, not a "may view status" one.
# Build + test everything (no root needed — the kernel is mocked in tests).
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
# Run the daemon (needs root for CAP_NET_ADMIN). For a throwaway state file:
sudo WIREFINDER_STATE=/tmp/wf-state.json target/debug/wirefinderd
# CLI against the running daemon:
target/debug/wirefinder import ./mullvad-nyc.conf # import a wg-quick config
target/debug/wirefinder add home <server_pubkey> vpn.example.com:51820 10.0.0.2/24 # generates a key
target/debug/wirefinder servers
target/debug/wirefinder switch home
# GUI dev:
cd ui && pnpm install && pnpm tauri devcd release && ./package.sh builds the daemon, CLI, and GUI and produces a single
.deb (cargo deb -p wirefinderd). The package installs and starts the daemon
right away — no config step — and the GUI handles the rest.
Pre-release note: the
state.jsonformat is not yet stable. This version moved the private key/address from a shared interface onto each server, so a state file from an earlier build won't load — delete/var/lib/wirefinder/state.json(and any devWIREFINDER_STATEfile) and re-onboard.