843 lines
74 KiB
Markdown
843 lines
74 KiB
Markdown
# wireguard-ui
|
||
|
||
## Contents
|
||
|
||
Quick links to the main documentation sections:
|
||
|
||
- [WireGuard UI v2](#wireguard-ui-v2) — what changed in this fork and how to switch language
|
||
- [Features](#features) — classic and v2 capabilities
|
||
- [HTTP API reference](#http-api-reference) — JSON routes, auth, and mobile-related endpoints
|
||
- [Run WireGuard-UI](#run-wireguard-ui) — binary and Docker Compose
|
||
- [Environment variables](#environment-variables) — configuration reference
|
||
- [Firebase Cloud Messaging (FCM)](#firebase-cloud-messaging-fcm) — push setup and registration API
|
||
- [Session idle timeout](#session-idle-timeout-settings--session--security) — minutes-based idle logout
|
||
- [Auto restart WireGuard daemon](#auto-restart-wireguard-daemon) — systemd / OpenRC / Docker
|
||
- [Build](#build) — assets, Docker image, binary
|
||
- [License](#license)
|
||
|
||
## WireGuard UI v2
|
||
|
||
This repository ships **version 2** of the WireGuard UI: an updated shell-style layout, richer monitoring and administration pages, Passkeys (WebAuthn) support, bilingual UI (English / Spanish via `locale/en.json` and `locale/es.json`), and extended optional OS integration (sysctl, `wg-quick` / `wg syncconf`, log tail) while keeping the same core purpose as the upstream project—manage peers, generate configs, and distribute them by QR, file, email, or Telegram.
|
||
|
||
**Notes**
|
||
|
||
- **Building from source**: run `./prepare_assets.sh` before `go build` when templates or static assets change (see **Build** below).
|
||
- **Changing UI language**: set **Language** under **Global settings**, save, click **Apply config** in the toolbar, then **reload the page** so server-rendered templates and client-side `WG_T` strings refresh.
|
||
|
||
---
|
||
|
||
A web user interface to manage your WireGuard setup.
|
||
|
||
## Features
|
||
|
||
### Classic capabilities (upstream parity)
|
||
|
||
- Web UI to manage WireGuard peers (create, edit, enable/disable, remove).
|
||
- Richer client records (name, email, notes, subnet ranges, Telegram user id, etc.).
|
||
- Distribute configs via **QR code**, **download**, **email**, or **Telegram**.
|
||
- Global defaults (endpoint, DNS, MTU, keepalive, config path) and server interface editor.
|
||
- Optional **Apply config** workflow to write `wg.conf` and optionally reload the kernel (`wg-quick` / `wg syncconf` when enabled).
|
||
|
||
### New in v2
|
||
|
||
- **Shell layout**: fixed sidebar + main content (`wgshell.css`), mobile-friendly nav, unified top bar with **Apply config** / pending-change handling.
|
||
- **Dashboard**: at-a-glance server/client KPIs, WireGuard presence, and actions that match live data (`/api/dashboard-stats`, restart helpers where configured).
|
||
- **Traffic**: bandwidth view backed by cached WireGuard counter samples (`/api/wg-traffic-series`), range presets, peer-aware charts.
|
||
- **Logs**: live sections when enabled (global "Logs" toggle)—optional file tail (`WGUI_LOG_TAIL_PATH`), `systemctl` / `journalctl` snippets for `wg-quick@...`, periodic refresh from `/api/system-logs`.
|
||
- **Status**: read-only peer table from `wgctrl` for quick inspection.
|
||
- **Global settings (expanded)**: configurable **session idle timeout** (minutes), **Passkeys** master toggle, **UI theme** (dark / light / auto), **UI language** (English / Spanish), **realtime stats** gate for Logs/Dashboard polling; staged save + apply flow with localStorage dirty tracking.
|
||
- **Internationalization**: strings in `locale/en.json` and `locale/es.json`; templates use `tr` / client bundle `WG_T` + `wgT()` for JS toasts and dynamic UI.
|
||
- **Multi-user auth**: **Users** admin page—create/edit/delete users, admin role, suspend account, revoke all sessions, inline Passkey add/remove/rename per user.
|
||
- **My account / Profile**: self-service display name, email, password change, own Passkeys.
|
||
- **Passkeys (WebAuthn)**: passwordless sign-in and registration flows; env knobs for RP ID / origins behind reverse proxies (`WGUI_WEBAUTHN_`*). Optional `**WGUI_ANDROID_PASSKEY_`*** + reverse-proxy wiring for `**/.well-known/assetlinks.json`** unlock the Flutter Android client's Credential Manager flows.
|
||
- **Server page extras** (Linux, when allowed): optional **IPv4 forwarding** via `sysctl`, **persist** / **auto-apply** preferences, `**wg-quick` down/up/restart** and `**wg syncconf`** after apply, optional **systemd**-based restarts.
|
||
- **Wake-on-LAN**: manage hosts and send magic packets from the UI.
|
||
- **Client list UX**: card layout with inline enable toggle, traffic chips fed by `**/api/wg-peer-stats`**, and "Apply config" integration after edits.
|
||
|
||
WireGuard UI v2
|
||
|
||
## HTTP API reference
|
||
|
||
Unless noted otherwise, paths are rooted at your configured `**BASE_PATH`** (empty at the site root, or e.g. `/wireguard`). Prefix every path below with that value.
|
||
|
||
**Sessions.** Most endpoints require a valid browser session cookie (`ValidSession`). A few JSON `POST` routes are public for Passkey login. JSON bodies expect `**Content-Type: application/json`** (this also mitigates simple CSRF from third-party sites).
|
||
|
||
**Admin-only** routes additionally require an administrator account (`NeedsAdmin` in the code).
|
||
|
||
### Well-known and health
|
||
|
||
|
||
| Method | Path | Auth | Purpose |
|
||
| ------------- | ------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||
| `GET`, `HEAD` | `/.well-known/assetlinks.json` | No | Digital Asset Links for Android Passkeys / Credential Manager (also mirrored under `BASE_PATH` when set; see comments in `main.go`). |
|
||
| `GET` | `{BASE}/_health` | No | Liveness probe. |
|
||
|
||
|
||
### Public and login
|
||
|
||
When login is **not** disabled:
|
||
|
||
|
||
| Method | Path | Auth | Purpose |
|
||
| ------ | ----------------------------------- | ---- | ---------------------------------------------- |
|
||
| `GET` | `{BASE}/login` | No | Login HTML. |
|
||
| `POST` | `{BASE}/login` | No | Password login (JSON). |
|
||
| `GET` | `{BASE}/api/public/login-wg-status` | No | WireGuard tunnel summary for the login banner. |
|
||
| `POST` | `{BASE}/api/passkeys/login/begin` | No | WebAuthn assertion options (JSON). |
|
||
| `POST` | `{BASE}/api/passkeys/login/finish` | No | Complete Passkey login (JSON). |
|
||
|
||
|
||
### Passkeys (authenticated)
|
||
|
||
|
||
| Method | Path | Notes |
|
||
| ------ | ----------------------------------------------- | ---------------------------------- |
|
||
| `POST` | `{BASE}/api/passkeys/register/:username/begin` | Start registration for `username`. |
|
||
| `POST` | `{BASE}/api/passkeys/register/:username/finish` | Finish registration. |
|
||
| `POST` | `{BASE}/api/passkeys/remove` | Remove a credential. |
|
||
| `POST` | `{BASE}/api/passkeys/rename` | Rename a credential. |
|
||
|
||
|
||
### Users and profile
|
||
|
||
|
||
| Method | Path | Notes |
|
||
| ------ | --------------------------------- | ------------------------------ |
|
||
| `POST` | `{BASE}/update-user` | Update profile fields. |
|
||
| `POST` | `{BASE}/create-user` | **Admin.** Create user. |
|
||
| `POST` | `{BASE}/remove-user` | **Admin.** Remove user. |
|
||
| `GET` | `{BASE}/get-users` | **Admin.** List users. |
|
||
| `GET` | `{BASE}/api/user/:username` | Fetch one user. |
|
||
| `GET` | `{BASE}/api/profile/passkeys` | Passkeys for the current user. |
|
||
| `POST` | `{BASE}/api/user/set-admin` | **Admin.** |
|
||
| `POST` | `{BASE}/api/user/set-disabled` | **Admin.** |
|
||
| `POST` | `{BASE}/api/user/revoke-sessions` | **Admin.** |
|
||
|
||
|
||
### Peers (clients)
|
||
|
||
|
||
| Method | Path | Notes |
|
||
| ------ | ----------------------------- | ---------------------------- |
|
||
| `GET` | `{BASE}/api/clients` | List all clients. |
|
||
| `GET` | `{BASE}/api/client/:id` | One client by numeric `id`. |
|
||
| `POST` | `{BASE}/new-client` | Create client. |
|
||
| `POST` | `{BASE}/update-client` | Update client. |
|
||
| `POST` | `{BASE}/remove-client` | Delete client. |
|
||
| `POST` | `{BASE}/client/set-status` | Enable/disable. |
|
||
| `POST` | `{BASE}/email-client` | Email configuration. |
|
||
| `POST` | `{BASE}/send-telegram-client` | Telegram delivery. |
|
||
| `GET` | `{BASE}/download` | Download one `.conf`. |
|
||
| `GET` | `{BASE}/download-all-configs` | **Admin.** ZIP of all peers. |
|
||
|
||
|
||
### Dashboard, stats, and traffic
|
||
|
||
|
||
| Method | Path | Notes |
|
||
| ------ | ------------------------------- | ------------------------------------------------------------------------------------ |
|
||
| `GET` | `{BASE}/api/dashboard-stats` | KPIs for the dashboard. |
|
||
| `GET` | `{BASE}/api/wg-peer-stats` | Per-peer counters for lists. |
|
||
| `GET` | `{BASE}/api/wg-traffic-series` | Cached series; query `range=24h` (default), `7d`, or `30d`. |
|
||
| `GET` | `{BASE}/api/machine-ips` | Suggested endpoint IPs. |
|
||
| `GET` | `{BASE}/api/subnet-ranges` | Ordered subnet ranges. |
|
||
| `GET` | `{BASE}/api/suggest-client-ips` | IP allocation hints. |
|
||
| `GET` | `{BASE}/api/ui-nav-hints` | Small JSON payload; useful as a **connectivity / session check** for native clients. |
|
||
|
||
|
||
### WireGuard server, apply, and tunnel control
|
||
|
||
|
||
| Method | Path | Notes |
|
||
| ------ | ------------------------------------ | -------------------------------------------- |
|
||
| `POST` | `{BASE}/wg-server/interfaces` | **Admin.** Save interface list from UI flow. |
|
||
| `POST` | `{BASE}/api/wg-server/save-page` | **Admin.** Combined “Server” tab JSON save. |
|
||
| `POST` | `{BASE}/wg-server/keypair` | **Admin.** Generate server keypair. |
|
||
| `POST` | `{BASE}/api/apply-wg-config` | Write `wg.conf` / apply workflow (JSON). |
|
||
| `GET` | `{BASE}/api/wireguard/tunnel-status` | Tunnel up/down and interface summary. |
|
||
| `POST` | `{BASE}/api/wireguard/wg-quick-down` | **Admin.** `wg-quick` down. |
|
||
| `POST` | `{BASE}/api/wireguard/wg-quick-up` | **Admin.** `wg-quick` up. |
|
||
|
||
|
||
### Push notifications (FCM)
|
||
|
||
|
||
| Method | Path | Body (JSON) |
|
||
| ------ | ---------------------------- | ----------------------------------------------------------- |
|
||
| `POST` | `{BASE}/api/push/register` | `{"token":"<FCM registration token>","platform":"android"}` |
|
||
| `POST` | `{BASE}/api/push/unregister` | `{"token":"<FCM registration token>"}` |
|
||
|
||
|
||
Requires a valid session. See [Firebase Cloud Messaging (FCM)](#firebase-cloud-messaging-fcm) for server env vars.
|
||
|
||
### Global settings and logs
|
||
|
||
|
||
| Method | Path | Notes |
|
||
| ------ | ------------------------------------------- | --------------------------------------- |
|
||
| `POST` | `{BASE}/global-settings` | **Admin.** Save global settings (JSON). |
|
||
| `POST` | `{BASE}/api/global-settings/realtime-stats` | **Admin.** Toggle realtime stats. |
|
||
| `GET` | `{BASE}/api/system-logs` | Log tail / snippets when enabled. |
|
||
|
||
|
||
### Wake-on-LAN
|
||
|
||
|
||
| Method | Path |
|
||
| -------- | -------------------------------------- |
|
||
| `POST` | `{BASE}/wake_on_lan_host` |
|
||
| `DELETE` | `{BASE}/wake_on_lan_host/:mac_address` |
|
||
| `PUT` | `{BASE}/wake_on_lan_host/:mac_address` |
|
||
|
||
|
||
### HTML pages (session)
|
||
|
||
These return HTML for the v2 shell, not JSON: `{BASE}/` (clients), `{BASE}/dashboard`, `{BASE}/traffic`, `{BASE}/logs`, `{BASE}/profile`, `{BASE}/users-settings` (**admin**), `{BASE}/wg-server` (**admin**), `{BASE}/global-settings` (**admin**), `{BASE}/status`, `{BASE}/wake_on_lan_hosts`, `{BASE}/about`. Use the JSON routes above for API integrations.
|
||
|
||
### Logout and misc
|
||
|
||
|
||
| Method | Path | Notes |
|
||
| ------ | ------------------ | ------------------------------------- |
|
||
| `GET` | `{BASE}/logout` | Ends session when login is enabled. |
|
||
| `GET` | `{BASE}/test-hash` | Internal/config hash probe (session). |
|
||
| `GET` | `{BASE}/favicon` | Favicon bytes. |
|
||
|
||
|
||
---
|
||
|
||
## Run WireGuard-UI
|
||
|
||
> ⚠️The default username and password are `admin`. Please change it to secure your setup.
|
||
|
||
### Using binary file
|
||
|
||
Download the binary file from the release page and run it directly on the host machine
|
||
|
||
```
|
||
./wireguard-ui
|
||
```
|
||
|
||
For a **persistent** install on Linux, register **systemd** as described in **[systemd: install and enable the web service](#systemd-install-and-enable-the-web-service)** below (working directory, database, environment files, permissions).
|
||
|
||
### Using docker compose
|
||
|
||
The [examples/docker-compose](examples/docker-compose) folder contains example docker-compose files.
|
||
Choose the example which fits you the most, adjust the configuration for your needs, then run it like below:
|
||
|
||
```
|
||
docker-compose up
|
||
```
|
||
|
||
## Environment Variables
|
||
|
||
|
||
| Variable | Description | Default |
|
||
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
|
||
| `BASE_PATH` | Set this variable if you run wireguard-ui under a subpath of your reverse proxy virtual host (e.g. /wireguard) | N/A |
|
||
| `BIND_ADDRESS` | The addresses that can access to the web interface and the port, use unix:///abspath/to/file.socket for unix domain socket. | 0.0.0.0:80 |
|
||
| `SESSION_SECRET` | The secret key used to encrypt the session cookies. Set this to a random value | N/A |
|
||
| `SESSION_SECRET_FILE` | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect | N/A |
|
||
| `SESSION_MAX_DURATION` | Max time in days a remembered session is refreshed and valid. Non-refreshed session is valid for 7 days max, regardless of this setting. | 90 |
|
||
| `SUBNET_RANGES` | The list of address subdivision ranges. Format: `SR Name:10.0.1.0/24; SR2:10.0.2.0/24,10.0.3.0/24` Each CIDR must be inside one of the server interfaces. | N/A |
|
||
| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` |
|
||
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` |
|
||
| `WGUI_PASSWORD_FILE` | Optional filepath for the user login password. Will be hashed automatically. Used for db initialization only. Leave `WGUI_PASSWORD` blank to take effect | N/A |
|
||
| `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A |
|
||
| `WGUI_PASSWORD_HASH_FILE` | Optional filepath for the user login password hash. (alternative to `WGUI_PASSWORD_FILE`). Used for db initialization only. Leave `WGUI_PASSWORD_HASH` blank to take effect | N/A |
|
||
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to. The endpoint can contain a port as well, useful when you are listening internally on the `WGUI_SERVER_LISTEN_PORT` port, but you forward on another port (ex 9000). Ex: myvpn.dyndns.com:9000 | Resolved to your public ip address |
|
||
| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo |
|
||
| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` |
|
||
| `WGUI_MTU` | The default MTU used in global settings | `1450` |
|
||
| `WGUI_PERSISTENT_KEEPALIVE` | The default persistent keepalive for WireGuard in global settings | `15` |
|
||
| `WGUI_FIREWALL_MARK` | The default WireGuard firewall mark | `0xca6c` (51820) |
|
||
| `WGUI_TABLE` | The default WireGuard table value settings | `auto` |
|
||
| `WGUI_CONFIG_FILE_PATH` | The default WireGuard config file path used in global settings | `/etc/wireguard/wg0.conf` |
|
||
| `WGUI_LOG_LEVEL` | The default log level. Possible values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF` | `INFO` |
|
||
| `WG_CONF_TEMPLATE` | The custom `wg.conf` config file template. Please refer to our [default template](https://github.com/ngoduykhanh/wireguard-ui/blob/master/templates/wg.conf) | N/A |
|
||
| `EMAIL_FROM_ADDRESS` | The sender email address | N/A |
|
||
| `EMAIL_FROM_NAME` | The sender name | `WireGuard UI` |
|
||
| `SENDGRID_API_KEY` | The SendGrid api key | N/A |
|
||
| `SENDGRID_API_KEY_FILE` | Optional filepath for the SendGrid api key. Leave `SENDGRID_API_KEY` blank to take effect | N/A |
|
||
| `SMTP_HOSTNAME` | The SMTP IP address or hostname | `127.0.0.1` |
|
||
| `SMTP_PORT` | The SMTP port | `25` |
|
||
| `SMTP_USERNAME` | The SMTP username | N/A |
|
||
| `SMTP_PASSWORD` | The SMTP user password | N/A |
|
||
| `SMTP_PASSWORD_FILE` | Optional filepath for the SMTP user password. Leave `SMTP_PASSWORD` blank to take effect | N/A |
|
||
| `SMTP_AUTH_TYPE` | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE` | `NONE` |
|
||
| `SMTP_ENCRYPTION` | The encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS` | `STARTTLS` |
|
||
| `SMTP_HELO` | Hostname to use for the HELO message. smtp-relay.gmail.com needs this set to anything but `localhost` | `localhost` |
|
||
| `TELEGRAM_TOKEN` | Telegram bot token for distributing configs to clients | N/A |
|
||
| `TELEGRAM_ALLOW_CONF_REQUEST` | Allow users to get configs from the bot by sending a message | `false` |
|
||
| `TELEGRAM_FLOOD_WAIT` | Time in minutes before the next conf request is processed | `60` |
|
||
| `FCM_CREDENTIALS_FILE` | Absolute path to the Firebase **service account** JSON used to send **FCM push** notifications (Android app tokens). If empty, `GOOGLE_APPLICATION_CREDENTIALS` is used instead. If neither resolves to a readable file, push is disabled. **Not** the same file as the app's `google-services.json`. | N/A |
|
||
| `GOOGLE_APPLICATION_CREDENTIALS` | Standard Google env: path to the **same** service account JSON as above. Used when `FCM_CREDENTIALS_FILE` is unset. | N/A |
|
||
|
||
|
||
### Firebase Cloud Messaging (FCM)
|
||
|
||
The server can send **push notifications** (e.g. peer created/removed/enabled/disabled, tunnel up/down) to devices that register an FCM token. Implementation lives in the `pushnotify` package. **FCM itself has no per-message charge** in typical Firebase usage; you still need a Firebase project and a service account for the Admin SDK.
|
||
|
||
#### Server setup
|
||
|
||
1. Open **[Firebase Console](https://console.firebase.google.com/)** → your project → **Project settings** (gear) → **Service accounts**.
|
||
2. Click **Generate new private key** and download the JSON. Store it on the server only (for example `/etc/wireguard-ui/firebase-adminsdk.json`). **Do not commit this file.** Restrict permissions (`chmod 600`, owned by the service user).
|
||
3. Point wireguard-ui at that file using `**FCM_CREDENTIALS_FILE`** (recommended) or `**GOOGLE_APPLICATION_CREDENTIALS`** (same path; used if `FCM_CREDENTIALS_FILE` is empty).
|
||
4. Restart wireguard-ui. You should see a log line such as `**FCM enabled`**. If credentials are missing or invalid, push stays off and an error is logged.
|
||
|
||
#### Project ID
|
||
|
||
The Firebase Go SDK needs a **Google Cloud / Firebase project ID**. It is normally taken from the `**project_id`** field inside the service account JSON. If you see `**project ID is required`** (or similar) in logs, set one of:
|
||
|
||
|
||
| Variable | Purpose |
|
||
| ---------------------- | -------------------------------------------------------------------- |
|
||
| `FIREBASE_PROJECT_ID` | Explicit Firebase/GCP project ID (highest precedence in app config). |
|
||
| `GOOGLE_CLOUD_PROJECT` | Same intent; common on GCP VMs. |
|
||
| `GCLOUD_PROJECT` | Same intent; legacy/alternate env name. |
|
||
|
||
|
||
Ensure the JSON file is the **service account key** from Firebase (it always contains `"type": "service_account"` and `"project_id"`).
|
||
|
||
#### Not the Android client file
|
||
|
||
- `**google-services.json`** is only for the **Android app** (Flutter `android/app/`). The **wireguard-ui server does not read it.** Server-side sending uses only the **service account** JSON from step 2.
|
||
|
||
#### Registration HTTP API
|
||
|
||
Authenticated JSON endpoints (same session cookies as the rest of the UI):
|
||
|
||
|
||
| Method | Path | Body |
|
||
| ------ | --------------------------------- | ----------------------------------------------------------- |
|
||
| `POST` | `{BASE_PATH}/api/push/register` | `{"token":"<FCM registration token>","platform":"android"}` |
|
||
| `POST` | `{BASE_PATH}/api/push/unregister` | `{"token":"<FCM registration token>"}` |
|
||
|
||
|
||
Registered tokens are persisted under the server DB directory (e.g. `**push_tokens.json`** next to other JSON store files).
|
||
|
||
#### Rate limiting
|
||
|
||
Outbound FCM sends are **rate-limited per device token** (for example **at most 3 notifications per minute per token**) to avoid flooding.
|
||
|
||
#### Flutter Android client
|
||
|
||
Configure Firebase for the app package name, place `**google-services.json`** in `**android/app/`**, and enable push in the app; see the companion repo `**wireguard-ui-android-client`** README.
|
||
|
||
### Session idle timeout (**Settings** → **Session & security**)
|
||
|
||
In the UI, **Session idle timeout (minutes)** is stored as `session_timeout_minutes` (integer). **Always use whole minutes—not seconds.**
|
||
|
||
|
||
| Item | Detail |
|
||
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| **Unit** | **Minutes**, range **5-1440** (about 24 h max). Example: enter `30` for ~30 minutes. |
|
||
| **Behavior** | **Idle logout:** after no authenticated HTTP request for longer than this time, the session is invalid (each request resets the idle clock). Applies to browsing and API endpoints that enforce `ValidSession`. |
|
||
| **When it applies** | After saving from **Settings** and confirming **Apply config**, new sessions use this value when users **log in again**. Log out or wait for expiry to observe the change immediately. |
|
||
| **Remember-me** | If a finite timeout is set in global settings, the login checkbox no longer lengthens the session to 7 days. |
|
||
| `**SESSION_MAX_DURATION`** | Separate hard cap on how long any session identity may persist (days from login), independent of idle timeout. See the env table above. |
|
||
|
||
|
||
### Defaults for server configuration
|
||
|
||
These environment variables are used to control the default server settings used when initializing the database.
|
||
|
||
|
||
| Variable | Description | Default |
|
||
| --------------------------------- | --------------------------------------------------------------------------------------------- | --------------- |
|
||
| `WGUI_SERVER_INTERFACE_ADDRESSES` | The default interface addresses (comma-separated-list) for the WireGuard server configuration | `10.252.1.0/24` |
|
||
| `WGUI_SERVER_LISTEN_PORT` | The default server listen port | `51820` |
|
||
| `WGUI_SERVER_POST_UP_SCRIPT` | The default server post-up script | N/A |
|
||
| `WGUI_SERVER_POST_DOWN_SCRIPT` | The default server post-down script | N/A |
|
||
|
||
|
||
### Defaults for new clients
|
||
|
||
These environment variables are used to set the defaults used in `New Client` dialog.
|
||
|
||
|
||
| Variable | Description | Default |
|
||
| ------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------- |
|
||
| `WGUI_DEFAULT_CLIENT_ALLOWED_IPS` | Comma-separated-list of CIDRs for the `Allowed IPs` field. (default ) | `0.0.0.0/0` |
|
||
| `WGUI_DEFAULT_CLIENT_EXTRA_ALLOWED_IPS` | Comma-separated-list of CIDRs for the `Extra Allowed IPs` field. (default empty) | N/A |
|
||
| `WGUI_DEFAULT_CLIENT_USE_SERVER_DNS` | Boolean value [`0`, `f`, `F`, `false`, `False`, `FALSE`, `1`, `t`, `T`, `true`, `True`, `TRUE`] | `true` |
|
||
| `WGUI_DEFAULT_CLIENT_ENABLE_AFTER_CREATION` | Boolean value [`0`, `f`, `F`, `false`, `False`, `FALSE`, `1`, `t`, `T`, `true`, `True`, `TRUE`] | `true` |
|
||
|
||
|
||
### Docker only
|
||
|
||
These environment variables only apply to the docker container.
|
||
|
||
|
||
| Variable | Description | Default |
|
||
| --------------------- | ------------------------------------------------------------- | ------- |
|
||
| `WGUI_MANAGE_START` | Start/stop WireGuard when the container is started/stopped | `false` |
|
||
| `WGUI_MANAGE_RESTART` | Auto restart WireGuard when we Apply Config changes in the UI | `false` |
|
||
|
||
|
||
### Server UI (optional OS integration)
|
||
|
||
Gate optional privileged actions invoked from the **Server** page (binary or Docker—the process must run on Linux with adequate permissions where needed):
|
||
|
||
|
||
| Variable | Description | Default |
|
||
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `WGUI_ALLOW_SYSCTL_IP_FORWARD` | When `true`, saving with **IPv4 forwarding (ip_forward)** may run `sysctl -w net.ipv4.ip_forward=1` / `...=0` on Linux. Without it, only the preference is stored in the database. Ignored outside Linux. | `false` |
|
||
| `WGUI_WG_SYNCCONF_AFTER_APPLY` | When `true`, **Apply config** runs `**wg-quick strip | wg syncconf `** on Linux so the running WireGuard matches the written file (e.g. disabling a client removes its peer from the server without` wg-quick down/up`). Requires` wg`and`wg-quick`on`$PATH`. If unset or` false`, Apply only writes the file/hash and does not reload kernel state. |
|
||
| `WGUI_ALLOW_WG_QUICK` | When `true`, **Apply** can run `wg-quick` down/up and the **Server** page shows **Stop** / **Start** / **Restart**. If unset, wg-quick controls are **off**. Start with `WGUI_ALLOW_WG_QUICK=true` when you intend to restart the tunnel from the UI. Env values are trimmed before parsing. | `false` |
|
||
| `WGUI_WG_RESTART_VIA_SYSTEMD` | On Linux, **Apply** prefers `systemctl restart wg-quick@ifac` when that unit exists (`LoadState=loaded`), so `**journalctl -u wg-quick@wg0`** shows restarts like a manual systemd restart. If `false` or no systemd, uses `wg-quick down`/`up`. | `true` |
|
||
| `WGUI_WGCONF_PENDING_WHEN_TUNNEL_STOPPED` | Linux: when Apply does **not** restart WireGuard while the netdev is absent/down (e.g. after **Stop**), the UI writes a side file next to `wg.conf` (suffix `.wgui-pending`) instead of overwriting the live `**WGUI_CONFIG_FILE_PATH`**. That avoids systemd `**.path`** units watching `wg.conf` that restart `wg-quick` on every save. `**wg-quick up**` or **Server › Start** merges the pending file into `wg.conf` first. Set `false` to always write `wg.conf` directly (legacy). | `true` |
|
||
| `WGUI_LOG_TAIL_PATH` | Optional absolute path to a log file shown in the **Logs** page. This variable is read-only: wireguard-ui does not write this file automatically. | *(unset)* |
|
||
| `WGUI_WEBAUTHN_RP_ID` | Optional fixed WebAuthn RP ID (recommended behind reverse proxy/public domain). If unset, it is inferred from request host. | *(auto)* |
|
||
| `WGUI_WEBAUTHN_RP_ORIGINS` | Optional comma-separated allowed origins for Passkeys (example: `https://vpn.example.com,https://admin.example.com`). If unset, origin is inferred per request. | *(auto)* |
|
||
| `WGUI_WEBAUTHN_RP_DISPLAY_NAME` | Optional WebAuthn RP display name shown by authenticators. | `WireGuard UI` |
|
||
| `WGUI_ANDROID_PASSKEY_SHA256` | One or more SHA-256 **signing-certificate fingerprints** of the Flutter/Android app (`./gradlew signingReport`), hex with or without colons, comma-separated. You may set this variable to an **absolute path** of a regular file (e.g. `/etc/wireguard-ui/android-SHA.secret`); the server reads the file contents as the same fingerprint string. The **wireguard-ui process user** must be able to read that file (e.g. `chgrp wireguard-ui` + `chmod 640`, or root-only if the service runs as root). Powers `**/.well-known/assetlinks.json`** (Digital Asset Links) and derives matching `**android:apk-key-hash:`** WebAuthn origins for native Android assertions. Unset ⇒ assetlinks endpoint returns 404 / no Android APK origins appended. | |
|
||
| `WGUI_ANDROID_PASSKEY_PACKAGE` | Android `applicationId` embedded in `**assetlinks.json`**. Omit to use `**com.wireguardui.wireguard_ui_client`**. | |
|
||
|
||
|
||
#### Troubleshooting: `wg-quick up` fails on `ip -6 route` / «Cannot find device wg0»
|
||
|
||
After toggling peers and **Start**, a failed half-bridge can leave routing in an odd state; the UI now runs `**wg-quick down`** (ignored if already down), waits briefly, then `**wg-quick up`**, and retries once if the first `up` still errors. If it persists, exclude `**wg0`** from **NetworkManager** / **systemd-networkd**, and ensure IPv6 is consistent (either working or intentionally off) with the `**Address`** line in `**wg.conf`**.
|
||
|
||
#### `WGUI_LOG_TAIL_PATH` quick setup (systemd)
|
||
|
||
Use this when you want the **Logs** page to also show a custom application log file.
|
||
|
||
1. Add environment variable to your wireguard-ui service:
|
||
|
||
```ini
|
||
[Service]
|
||
Environment="WGUI_LOG_TAIL_PATH=/var/log/wireguard-ui.log"
|
||
```
|
||
|
||
1. Ensure file exists and is readable by the service user:
|
||
|
||
```bash
|
||
sudo touch /var/log/wireguard-ui.log
|
||
sudo chmod 640 /var/log/wireguard-ui.log
|
||
```
|
||
|
||
1. (Recommended) append service stdout/stderr to that file:
|
||
|
||
```ini
|
||
[Service]
|
||
StandardOutput=append:/var/log/wireguard-ui.log
|
||
StandardError=append:/var/log/wireguard-ui.log
|
||
```
|
||
|
||
1. Reload and restart:
|
||
|
||
```bash
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl restart wireguard-ui
|
||
```
|
||
|
||
1. Verify:
|
||
|
||
```bash
|
||
sudo systemctl show wireguard-ui -p Environment
|
||
sudo tail -n 50 /var/log/wireguard-ui.log
|
||
```
|
||
|
||
> Note: The Logs page now also includes `systemctl status wg-quick@<iface>` and recent `journalctl` output. `WGUI_LOG_TAIL_PATH` is only for the optional file section.
|
||
|
||
#### Passkeys (WebAuthn) behind reverse proxy (systemd example)
|
||
|
||
If you use a public domain and/or reverse proxy (Nginx, Caddy, Traefik, Cloudflare Tunnel), define a fixed WebAuthn RP ID and allowed origins. Add the following under `**[Service]**` (for example with `**systemctl edit wireguard-ui**` or entries in the same `**EnvironmentFile=**` you use in **[systemd: install and enable the web service](#systemd-install-and-enable-the-web-service)**):
|
||
|
||
```ini
|
||
[Service]
|
||
Environment="WGUI_WEBAUTHN_RP_ID=vpn.example.com"
|
||
Environment="WGUI_WEBAUTHN_RP_ORIGINS=https://vpn.example.com"
|
||
Environment="WGUI_WEBAUTHN_RP_DISPLAY_NAME=WireGuard UI"
|
||
```
|
||
|
||
Then reload and restart:
|
||
|
||
```bash
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl restart wireguard-ui
|
||
```
|
||
|
||
Notes:
|
||
|
||
- `WGUI_WEBAUTHN_RP_ID` must match your effective login domain.
|
||
- `WGUI_WEBAUTHN_RP_ORIGINS` accepts comma-separated values for multi-origin setups.
|
||
- Passkeys require `https://` in production (browsers only allow non-HTTPS for localhost).
|
||
|
||
##### Android app passkeys (companion Flutter client — Credential Manager)
|
||
|
||
The Flutter Android companion app calls the same WireGuard UI WebAuthn JSON endpoints (`**POST**` `**{BASE_PATH}/api/passkeys/login/begin**`, `**/finish**`). Android **Credential Manager** still verifies **Digital Asset Links** independently: it downloads `**https://<rpId>/.well-known/assetlinks.json`** and checks package name + signing certificate fingerprints against your app install.
|
||
|
||
Assertion signatures include `**Origin`** inside `**clientDataJSON`**. Browser users send origins like `**https://vpn.example.com`**. Native Android sends `**android:apk-key-hash:<base64url(SHA-256(signing-cert-digest))>**`. The `**go-webauthn**` verifier must therefore allow both your HTTPS origins and the APK-hash origin — WireGuard UI appends APK-hash origins whenever `**WGUI_ANDROID_PASSKEY_SHA256**` is set.
|
||
|
||
**Suggested end-to-end flow**
|
||
|
||
1. **Browser first:** In WireGuard UI, enable **Passkeys** under Global settings and **Apply config**. Open the panel URL (example: `**https://vpn.example.net/wg`** if `BASE_PATH` is `**/wg`**), visit Profile (or Administrator → Users), and enroll at least one passkey per account that should unlock the Android app. That registers credentials bound to `**WGUI_WEBAUTHN_RP_ID`** (or inferred host).
|
||
2. **Server env:** Set `**WGUI_WEBAUTHN_RP_ORIGINS`** (comma-separated `**https://`** origins visitors actually see) plus `**WGUI_WEBAUTHN_RP_ID`** when you rely on proxies or internal hostnames — they must mirror the HTTPS hostname tied to `**assetlinks.json`**. Populate `**WGUI_ANDROID_PASSKEY_SHA256**` from `./gradlew signingReport`, matching whichever keystore ships on devices, and set `**WGUI_ANDROID_PASSKEY_PACKAGE**` if Gradle `**applicationId**` deviates from the default `**com.wireguardui.wireguard_ui_client**`.
|
||
3. **Reverse proxy:** Route `**/.well-known/assetlinks.json`** on the panel hostname back to WireGuard UI (see examples below).
|
||
4. **App:** Configure base URL/base path matching your API prefix. Fill **Passkey origin** if the HTTPS hostname where you enrolled passkeys differs from the API `**Host`** (LAN IP/API gateway case). Prefer **Username** + passkey together if the credential is not discoverable (common for web-created passkeys unless you mandated resident/discoverable enrollment).
|
||
|
||
**Symptoms resolved by proper setup**
|
||
|
||
|
||
| Symptom | Typical cause |
|
||
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `**RP ID cannot be validated`** (Credential Manager DOM error) | Missing or inaccessible `**https://<rpId>/.well-known/assetlinks.json`** (**404**, auth wall, redirects). Proxies forwarding only `**{BASE_PATH}`** strand this path unless you terminate `**/.well-known`** upstream—see **[Digital Asset Links](https://developers.google.com/digital-asset-links/v1/getting-started)** tooling to validate statements externally. |
|
||
| `**Invalid passkey`** after tapping a credential but **Digital Asset Links** already pass | Older servers missing APK-hash `**RPOrigins`**, malformed assertion body, mismatched `**WGUI_ANDROID_PASSKEY_SHA256`** vs APK, or `**begin`/`finish` session loss** — see cookie note below. |
|
||
| `**HTTP 405`** (`curl -I` only) against assetlinks | `curl -I` issues **HEAD**. WireGuard UI implements **HEAD** + **GET**; if you proxy strips HEAD pick **GET** (`**curl -sS`**). |
|
||
|
||
|
||
**Server configuration checklist**
|
||
|
||
1. `**WGUI_ANDROID_PASSKEY_SHA256`** — fingerprint(s) comma-separated (**hex**, with or without colons), matching Android Studio / `./gradlew signingReport` (**SHA-256**) for whatever build ships to devices; or an **absolute path** to a file whose contents are that string (readable by the **wireguard-ui** process user).
|
||
2. `**WGUI_ANDROID_PASSKEY_PACKAGE`** — optional `**applicationId`**, defaults to `**com.wireguardui.wireguard_ui_client`** inside `**assetlinks.json**`.
|
||
3. **Reverse proxy exposes host-root asset links** proxying `**https://<hostname>/.well-known/assetlinks.json`** to WireGuard UI's `**http://backend:PORT/.well-known/assetlinks.json`**. `**https://hostname/wg/.well-known`** is **ignored** by Android's association crawler.
|
||
4. **Mirror workaround:** If you truly cannot terminate `**/.well-known`** directly on WireGuard UI, `**GET https://vpn.example.net/wg/.well-known/assetlinks.json`** (duplicate route emitted when `**BASE_PATH=/wg`**; adjust path for your `**BASE_PATH`**) returns the identical JSON blob you can synchronize to `**https://vpn.example.net/.well-known/assetlinks.json**` elsewhere.
|
||
|
||
**Behavior implemented in WireGuard UI**
|
||
|
||
|
||
| Mechanism | Role |
|
||
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `**GET` / `HEAD`** `**/.well-known/assetlinks.json`** | Issues Digital Asset Links JSON including `**delegate_permission/common.get_login_creds`** plus `**delegate_permission/common.handle_all_urls`**. |
|
||
| `**X-WGUI-WebAuthn-Public-Origin**` | Optional HTTPS-only hint validated against configured origins; forces rp host alignment when mobiles talk to `**https://LAN:port/wg**` but passkeys bind to `**https://vpn.example.com**`. If it disagrees with `**WGUI_WEBAUTHN_RP_ID**`, the hinted public host wins **for configuring WebAuthn** so Credential Manager `**rp.id`** verification matches `**assetlinks`**. Never trust arbitrary hosts blindly — values must already be admitted via `**WGUI_WEBAUTHN_RP_ORIGINS`**, `**WGUI_WEBAUTHN_RP_ID` hostname match**, or the inferred default origin header. |
|
||
| `**android:apk-key-hash`** derived origins | For every SHA-256 entry in `**WGUI_ANDROID_PASSKEY_SHA256`**, append `**android:apk-key-hash:`** + URL-safe Base64 (no padding) of the raw digest to `**RPOrigins**`. |
|
||
| `**RPAllowCrossOrigin`** | Enabled whenever `**WGUI_ANDROID_PASSKEY_SHA256`** is non-empty so Credential Manager payloads that declare `**crossOrigin`** in `**clientDataJSON**` pass verification once origins match. |
|
||
|
||
|
||
**Reverse proxy snippets**
|
||
|
||
**Caddy (keep `/.well-known` on the apex host while `{BASE_PATH}` serves the UI — e.g. `/wg`)**
|
||
|
||
```caddyfile
|
||
vpn.example.net {
|
||
handle /.well-known/assetlinks.json {
|
||
reverse_proxy 127.0.0.1:5000
|
||
}
|
||
handle /wg* {
|
||
reverse_proxy 127.0.0.1:5000
|
||
}
|
||
}
|
||
```
|
||
|
||
Replace `**127.0.0.1:5000**` with your `**BIND_ADDRESS**` target (`docker` internal hostname, upstream socket, etc.).
|
||
|
||
**Apache / legacy `ProxyPass`**
|
||
|
||
Enable `**mod_proxy**` + `**proxy_http**` (Debian/Ubuntu: `a2enmod proxy proxy_http`). Example:
|
||
|
||
```apache
|
||
SSLProxyEngine on
|
||
ProxyPass /.well-known/assetlinks.json http://127.0.0.1:5000/.well-known/assetlinks.json
|
||
ProxyPassReverse /.well-known/assetlinks.json http://127.0.0.1:5000/.well-known/assetlinks.json
|
||
```
|
||
|
||
Place specific statements **above** wildcard `**ProxyPass /`** directives.
|
||
|
||
Equivalent **Nginx** pattern:
|
||
|
||
```nginx
|
||
location = /.well-known/assetlinks.json {
|
||
proxy_pass http://127.0.0.1:5000/.well-known/assetlinks.json;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
```
|
||
|
||
Verification (expect `**200**` and JSON body):
|
||
|
||
```bash
|
||
curl -sSIL https://vpn.example.net/.well-known/assetlinks.json
|
||
curl -sS https://vpn.example.net/.well-known/assetlinks.json
|
||
# pipe through jq locally if installed for readability
|
||
```
|
||
|
||
**Login session cookie**
|
||
|
||
The `**login/begin`** response sets a `**session`** cookie tying server-side `**SessionData`** to `**login/finish`**. The companion Flutter client keeps a Dio cookie jar; ensure reverse proxies propagate `**Cookie` / `Set-Cookie**` without stripping attributes. Tune `**WGUI_SESSION_COOKIE_***` when browsers or SPA clients authenticate cross-site (`SameSite=None` + `Secure`).
|
||
|
||
Logs: failing `**finish**` emits `**WARN [passkeys] login finish rejected ...**` with the verifier error (**origin**, **challenge**, **signature**) — correlate with timestamps while reproducing mobile flows.
|
||
|
||
Companion UX reminders:
|
||
|
||
- `**Passkey origin`** on the Flutter login sheet maps straight to `**X-WGUI-WebAuthn-Public-Origin`** whenever the HTTPS hostname used during browser enrollment differs from the API `**Host`**.
|
||
- Supply `**Username`** + passkey for accounts whose authenticators are **non-discoverable** (typical hybrid web enrollments unless you forced resident keys).
|
||
- After rotating TLS/proxy fingerprints, run `**adb shell pm verify-app-links --re-verify com.wireguardui.wireguard_ui_client`** so Android re-fetches statements (swap package id if customized).
|
||
|
||
Additional documentation lives in `**wireguard-ui-android-client`** `README.md` (mobile-focused recap).
|
||
|
||
##### Caddy + Dynamic DNS (No-IP): quick HTTPS so Passkeys work
|
||
|
||
Browsers treat Passkeys/WebAuthn as **[secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts)** (`https://` on a hostname, or `http://localhost`). Plain `http://<your-ip>` is **not** enough. Use a hostname (No-IP, DuckDNS, etc.), forward ports **80** and **443**, and terminate TLS with Caddy.
|
||
|
||
1. **No-IP (or similar)**
|
||
Create `yourhost.ddns.net` (example), install the updater or rely on No-IP so the **A record** points to your **WAN** public IP.
|
||
2. **Firewall / router**
|
||
Forward **TCP 80** and **TCP 443** from the internet to the machine that runs Caddy (required for Let's Encrypt HTTP-01 by default).
|
||
3. **Install Caddy**
|
||
Follow [Caddy install docs](https://caddyserver.com/docs/install) for your distro (official repo or package).
|
||
4. `**Caddyfile`** (minimal reverse proxy to WireGuard UI on loopback):
|
||
```caddyfile
|
||
yourhost.ddns.net {
|
||
encode gzip
|
||
reverse_proxy 127.0.0.1:5000
|
||
}
|
||
```
|
||
Replace `yourhost.ddns.net` with your hostname and `**5000**` with the port where `wireguard-ui` listens (`BIND_ADDRESS`, e.g. `:5000` or `127.0.0.1:5000`).
|
||
5. **(Optional, recommended)** Listen only on localhost so only Caddy exposes HTTPS:
|
||
```bash
|
||
BIND_ADDRESS=127.0.0.1:5000 ./wireguard-ui
|
||
```
|
||
6. **Restart Caddy**, then open `**https://yourhost.ddns.net`** and confirm the browser shows a **valid lock** (no certificate warnings).
|
||
7. `**wireguard-ui` systemd** — set RP ID/origin to match **exactly** what users type in the browser:
|
||
```ini
|
||
[Service]
|
||
Environment="WGUI_WEBAUTHN_RP_ID=yourhost.ddns.net"
|
||
Environment="WGUI_WEBAUTHN_RP_ORIGINS=https://yourhost.ddns.net"
|
||
```
|
||
Then `daemon-reload` and `restart wireguard-ui`.
|
||
8. **Inside the UI** — **Settings** → enable **Passkeys** → **Apply config**. Then **Administration → Users**: register a passkey per user. The login page will offer **Sign in with Passkey** once enabled.
|
||
|
||
If HTTPS still fails behind NAT, verify port 80 reaches Caddy on first certificate issuance; use `journalctl -u caddy -f` on errors.
|
||
|
||
## Auto restart WireGuard daemon
|
||
|
||
WireGuard-UI only takes care of configuration generation. On Linux you can enable in-process `wg syncconf` after apply (see variables above), or use systemd to watch for changes and restart the
|
||
service. Following is an example:
|
||
|
||
> **Note:** The **systemd** block below does **not** start the `wireguard-ui` web process. It only runs `systemctl restart wg-quick@wg0` when `wg0.conf` is modified on disk. The UI binary is a separate program (see **Run WireGuard-UI** above and **[systemd: install and enable the web service](#systemd-install-and-enable-the-web-service)**).
|
||
|
||
### systemd: install and enable the web service
|
||
|
||
This section is about the **wireguard-ui HTTP process** (the web UI), not the optional `**wg-quick@`** watcher described later.
|
||
|
||
#### What systemd must provide
|
||
|
||
1. **Working directory** — The app opens its JSON store at `**./db`** relative to the current working directory (`jsondb.New("./db")` in `main.go`). The unit **must** set `WorkingDirectory` to a persistent directory owned by the service user (e.g. `/var/lib/wireguard-ui`). If you omit this, the database lands wherever systemd’s default cwd is (often `/` or `/root`), which is easy to misplace or permission incorrectly.
|
||
2. **Binary** — Install the release binary (or your own build after `./prepare_assets.sh`) to a fixed path, e.g. `**/usr/local/bin/wireguard-ui`**, mode `0755`.
|
||
3. **Environment** — All knobs (`BASE_PATH`, `BIND_ADDRESS`, `SESSION_SECRET`, `WGUI_`*, `FCM_CREDENTIALS_FILE`, etc.) are ordinary **process environment variables**. Set them with `Environment=` lines in the unit, or load a file with `**EnvironmentFile=`**.
|
||
|
||
#### Register the service (step by step)
|
||
|
||
1. **Create an unprivileged account and data directory** (recommended). The home directory doubles as `**WorkingDirectory`** / database location:
|
||
```bash
|
||
sudo useradd --system --create-home --home-dir /var/lib/wireguard-ui \
|
||
--shell /usr/sbin/nologin --user-group wireguard-ui
|
||
sudo chmod 750 /var/lib/wireguard-ui
|
||
```
|
||
If the user already exists, ensure `**/var/lib/wireguard-ui**` exists and is owned by `**wireguard-ui:wireguard-ui**` with mode `**0750**`.
|
||
2. **Install the binary**:
|
||
```bash
|
||
sudo install -m 0755 wireguard-ui /usr/local/bin/wireguard-ui
|
||
```
|
||
3. **Optional config directory** for secrets on disk (session key, Firebase JSON, Android SHA file, etc.):
|
||
```bash
|
||
sudo mkdir -p /etc/wireguard-ui
|
||
sudo chown root:wireguard-ui /etc/wireguard-ui
|
||
sudo chmod 750 /etc/wireguard-ui
|
||
```
|
||
Place secret files here and grant the **service user** read access (e.g. `chmod 640` and group `wireguard-ui`, or ownership `wireguard-ui:wireguard-ui` as appropriate). If a path is unreadable by the process user, features that read that file (session encryption, FCM, passkey asset links) will fail at runtime.
|
||
4. **Environment file** — systemd reads `**KEY=value`** lines from `EnvironmentFile=` (comments with `#` allowed). You do **not** need `export`. Example `**/etc/default/wireguard-ui`** (Debian/Ubuntu naming is common; the path is arbitrary as long as the unit references it):
|
||
```text
|
||
BIND_ADDRESS=127.0.0.1:5000
|
||
BASE_PATH=wg
|
||
SESSION_SECRET_FILE=/etc/wireguard-ui/session.secret
|
||
```
|
||
Point `SESSION_SECRET` **or** `SESSION_SECRET_FILE` at a strong secret (see the environment table above). Same idea for `WGUI_PASSWORD_FILE`, `FCM_CREDENTIALS_FILE`, and `**WGUI_ANDROID_PASSKEY_SHA256`** (inline hex **or** absolute path to a file whose contents are the fingerprint string).
|
||
5. **Unit file** — Create `**/etc/systemd/system/wireguard-ui.service`**:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=WireGuard UI
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=wireguard-ui
|
||
Group=wireguard-ui
|
||
WorkingDirectory=/var/lib/wireguard-ui
|
||
EnvironmentFile=-/etc/default/wireguard-ui
|
||
ExecStart=/usr/local/bin/wireguard-ui
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
The `-` prefix on `**EnvironmentFile=-/etc/default/wireguard-ui**` means “ignore if missing” so the unit still parses before you create the file. You can instead use `**/etc/wireguard-ui.env**` or multiple `Environment="KEY=value"` lines for a minimal setup.
|
||
|
||
1. **Reload systemd and start**:
|
||
```bash
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now wireguard-ui
|
||
sudo systemctl status wireguard-ui
|
||
```
|
||
2. **Logs**:
|
||
```bash
|
||
journalctl -u wireguard-ui -f
|
||
```
|
||
|
||
#### WireGuard config and `wg-quick` / `systemctl`
|
||
|
||
If the UI should **write** `wg0.conf` (default `**/etc/wireguard/wg0.conf`**) or run `**wg-quick`** / `**systemctl restart wg-quick@…**`, the `**wireguard-ui` user** must be allowed to do so on your distribution (group membership on `/etc/wireguard`, `sudoers` for specific commands, or a documented choice to run the service as root — discouraged). There is no single recipe across distros; tighten permissions after verifying **Apply config** and optional `**WGUI_ALLOW_WG_QUICK`** / `**WGUI_WG_RESTART_VIA_SYSTEMD`** behaviour.
|
||
|
||
#### Example unit without `EnvironmentFile` (inline bind only)
|
||
|
||
The app stores its JSON database under `**./db` relative to the process working directory**, so `WorkingDirectory` is mandatory for a predictable data path.
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=WireGuard UI
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=wireguard-ui
|
||
Group=wireguard-ui
|
||
WorkingDirectory=/var/lib/wireguard-ui
|
||
Environment="BIND_ADDRESS=127.0.0.1:5000"
|
||
ExecStart=/usr/local/bin/wireguard-ui
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
Then `systemctl daemon-reload` and `systemctl enable --now wireguard-ui` as above.
|
||
|
||
### Using systemd (restart `wg-quick` when config file changes)
|
||
|
||
Create `/etc/systemd/system/wgui.service`
|
||
|
||
```bash
|
||
cd /etc/systemd/system/
|
||
cat << EOF > wgui.service
|
||
[Unit]
|
||
Description=Restart WireGuard
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/usr/bin/systemctl restart wg-quick@wg0.service
|
||
|
||
[Install]
|
||
RequiredBy=wgui.path
|
||
EOF
|
||
```
|
||
|
||
Create `/etc/systemd/system/wgui.path`
|
||
|
||
```bash
|
||
cd /etc/systemd/system/
|
||
cat << EOF > wgui.path
|
||
[Unit]
|
||
Description=Watch /etc/wireguard/wg0.conf for changes
|
||
|
||
[Path]
|
||
PathModified=/etc/wireguard/wg0.conf
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
```
|
||
|
||
Apply it
|
||
|
||
```sh
|
||
systemctl enable wgui.{path,service}
|
||
systemctl start wgui.{path,service}
|
||
```
|
||
|
||
### Using openrc
|
||
|
||
Create `/usr/local/bin/wgui` file and make it executable
|
||
|
||
```sh
|
||
cd /usr/local/bin/
|
||
cat << EOF > wgui
|
||
#!/bin/sh
|
||
wg-quick down wg0
|
||
wg-quick up wg0
|
||
EOF
|
||
chmod +x wgui
|
||
```
|
||
|
||
Create `/etc/init.d/wgui` file and make it executable
|
||
|
||
```sh
|
||
cd /etc/init.d/
|
||
cat << EOF > wgui
|
||
#!/sbin/openrc-run
|
||
|
||
command=/sbin/inotifyd
|
||
command_args="/usr/local/bin/wgui /etc/wireguard/wg0.conf:w"
|
||
pidfile=/run/${RC_SVCNAME}.pid
|
||
command_background=yes
|
||
EOF
|
||
chmod +x wgui
|
||
```
|
||
|
||
Apply it
|
||
|
||
```sh
|
||
rc-service wgui start
|
||
rc-update add wgui default
|
||
```
|
||
|
||
### Using Docker
|
||
|
||
Set `WGUI_MANAGE_RESTART=true` to manage Wireguard interface restarts.
|
||
Using `WGUI_MANAGE_START=true` can also replace the function of `wg-quick@wg0` service, to start Wireguard at boot, by
|
||
running the container with `restart: unless-stopped`. These settings can also pick up changes to Wireguard Config File
|
||
Path, after restarting the container. Please make sure you have `--cap-add=NET_ADMIN` in your container config to make
|
||
this feature work.
|
||
|
||
## Build
|
||
|
||
### Build docker image
|
||
|
||
Go to the project root directory and run the following command:
|
||
|
||
```sh
|
||
docker build --build-arg=GIT_COMMIT=$(git rev-parse --short HEAD) -t wireguard-ui .
|
||
```
|
||
|
||
or
|
||
|
||
```sh
|
||
docker compose build --build-arg=GIT_COMMIT=$(git rev-parse --short HEAD)
|
||
```
|
||
|
||
:information_source: A container image is available on [Docker Hub](https://hub.docker.com/r/ngoduykhanh/wireguard-ui)
|
||
which you can pull and use
|
||
|
||
```
|
||
docker pull ngoduykhanh/wireguard-ui
|
||
```
|
||
|
||
### Build binary file
|
||
|
||
Prepare the assets directory
|
||
|
||
```sh
|
||
./prepare_assets.sh
|
||
```
|
||
|
||
Then build your executable
|
||
|
||
```sh
|
||
go build -o wireguard-ui
|
||
```
|
||
|
||
## License
|
||
|
||
MIT. See [LICENSE](https://github.com/ngoduykhanh/wireguard-ui/blob/master/LICENSE).
|
||
|
||
## Support
|
||
|
||
If you like the project and want to support it, you can *buy me a coffee* ☕ |