VPN management panel for VPS servers. Single binary with an embedded, server-rendered web UI. Currently supports WireGuard, with more protocols planned.
- Create and manage WireGuard networks and peers; bring interfaces up or down from the web UI or API
- Download peer
.conffiles or display them as a QR code - The web UI is server-rendered (templ + Tailwind) and compiled into the binary at build time, one file to deploy
- Admin credentials come from environment variables and are bootstrapped on startup
- Ships with
compose.ymlfor one-command deployment
| OS | Status | Notes |
|---|---|---|
| Linux | Stable | Full support via netlink + wgctrl |
| FreeBSD / OpenBSD | Experimental | Userspace WireGuard via golang.zx2c4.com/wireguard; OpenBSD interface names are auto-generated (tun0, tun1, …) since tun(4) can't be renamed |
| macOS (Darwin) | Experimental | Userspace WireGuard via golang.zx2c4.com/wireguard; requires utun interface support |
| Windows | Experimental | Userspace WireGuard via golang.zx2c4.com/wireguard; address configuration via the IP Helper API (winipcfg) |
Experimental platforms compile and run but haven't been tested in production. Interface state is in-memory only and lost on process restart; re-running "up" after a restart may conflict with the still-running kernel interface.
cp .env.example .env
# Edit .env: set HOST_ADDRESS, ADMIN_USERNAME, ADMIN_PASSWORD at minimum
docker compose up -dOpen http://localhost:8080 and log in with your admin credentials.
All required capabilities (
NET_ADMIN,/dev/net/tun,net.ipv4.ip_forward) are pre-configured incompose.yml.
Builds run through mise: it pins the exact Go and Bun versions
(mise.toml) and downloads them itself on first run, so nothing needs to be pre-installed.
Same command on Linux, macOS, or Windows.
cp .env.example .env
# Edit .env
curl https://mise.run | sh # or: winget install jdx.mise / scoop install mise / brew install mise
mise run build # generate (sqlc + templ) -> bundle frontend deps (assets/ + templui via bun) -> go build -o app ./cmd/api
sudo ./app # NET_ADMIN privileges required for WireGuardOther tasks: mise run install, mise run generate, mise run assets, mise run run,
mise run clean. See mise.toml for the full task graph.
All configuration is via environment variables (loaded from .env if present). Copy .env.example to .env:
| Variable | Default | Description |
|---|---|---|
SQLITE_DATABASE_PATH |
./store.db |
Path to the SQLite database file |
HOST_ADDRESS |
1.2.3.4 |
Public IP of this server, embedded in generated peer configs |
HTTP_ADDRESS |
:8080 |
HTTP listen address |
GRACEFUL_TIMEOUT |
5s |
Graceful shutdown timeout |
ADMIN_USERNAME |
admin |
Bootstrap admin username (upserted on every boot) |
ADMIN_PASSWORD |
password |
Bootstrap admin password (upserted on every boot) |
ADMIN_PASSWORD_HASH |
(unset) | Pre-computed bcrypt hash of the admin password; if set, takes priority over ADMIN_PASSWORD |
ENVIRONMENT |
dev |
Set to prod to disable debug logging and enforce the session cookie's Secure flag |
To avoid keeping a plaintext password in .env, generate a bcrypt hash with htpasswd (from apache2-utils on Debian/Ubuntu, httpd-tools on RHEL/Fedora) and set it as ADMIN_PASSWORD_HASH instead of ADMIN_PASSWORD:
htpasswd -bnBC 12 "" 'your-password' | cut -d: -f2The JSON API lives under /api. The web UI itself is server-rendered separately (/, /login, /wireguard/...) and isn't part of this API. Protected routes require an active session cookie.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/health |
No | Health check |
POST |
/api/auth/login |
No | Log in |
POST |
/api/auth/logout |
No | Log out |
GET |
/api/vpn/wireguard/networks |
Yes | List WireGuard networks |
POST |
/api/vpn/wireguard/networks/generate |
Yes | Create a new network |
GET |
/api/vpn/wireguard/networks/:id |
Yes | Get network details |
POST |
/api/vpn/wireguard/networks/:id/up |
Yes | Bring interface up |
POST |
/api/vpn/wireguard/networks/:id/down |
Yes | Bring interface down |
DELETE |
/api/vpn/wireguard/networks/:id |
Yes | Delete network |
POST |
/api/vpn/wireguard/networks/:id/peers/generate |
Yes | Add peer to network |
GET |
/api/vpn/wireguard/peers/:peerId/config |
Yes | Get peer config text |
GET |
/api/vpn/wireguard/peers/:peerId/config/download |
Yes | Download peer .conf file |
GET |
/api/vpn/wireguard/peers/:peerId/qr |
Yes | Get peer config as a QR code image |
DELETE |
/api/vpn/wireguard/peers/:peerId |
Yes | Remove peer |
cmd/api/ # Binary entrypoint (main.go)
internal/
app/ # Application wiring, route registration
auth/ # Auth handler, service, JSON API routes
user/ # Bootstrap admin user, repository, service
wireguard/ # WireGuard networks/peers: netlink + wgctrl, JSON API routes
webui/ # Server-rendered UI routes/handlers, embedded static assets
config/ # Environment-variable configuration
middleware/ # Auth, error handling
store/ # sqlc-generated DB layer + goose migration runner
validator/ # go-playground/validator adapter for Fiber
logger/ # slog setup
view/
pages/ # templ page templates
layouts/ # templ layout templates
components/ui/ # vendored templui component set
db/
migrations/ # goose SQL migrations
queries/ # sqlc input queries
assets/ # Frontend dependency sources (Tailwind v4 entrypoint), bundled via bun into internal/webui/static/app.css
mise.toml # Pinned Go/Bun toolchain + build tasks (mise run build)
- MySQL as an alternative to SQLite
- Additional VPN protocols beyond WireGuard
- Stable non-Linux support (persistent interface state across restarts)
VoidShift is dual-licensed under MIT or Apache License 2.0, at your option.