From 644f027756ab70332e9b1efeb5de4197d717f014 Mon Sep 17 00:00:00 2001 From: Skyline Date: Sun, 3 May 2026 21:35:41 -0600 Subject: [PATCH] Document HTTP API; align Android session idle with companion app Add README table of contents and HTTP API reference material. Session cookies respect a higher idle floor for requests with X-WGUI-Client: android; passkey login uses the same effective idle. --- README.md | 555 +++++++++++++++++++++++--------------------- handler/passkeys.go | 3 + handler/session.go | 49 +++- 3 files changed, 325 insertions(+), 282 deletions(-) diff --git a/README.md b/README.md index 32b700f..15def2e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -![](https://github.com/ngoduykhanh/wireguard-ui/workflows/wireguard-ui%20build%20release/badge.svg) - # wireguard-ui ## Contents @@ -51,126 +49,146 @@ A web user interface to manage your WireGuard setup. - **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. +- **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. +- **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](https://github.com/user-attachments/assets/b4454f2d-21ae-4d36-89b6-19c1260a930b) +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. +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). +**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. | + +| 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). | + +| 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. | + +| 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.** | + +| 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. | + +| 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. | + +| 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. | + +| 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":"","platform":"android"}` | -| `POST` | `{BASE}/api/push/unregister` | `{"token":""}` | + +| Method | Path | Body (JSON) | +| ------ | ---------------------------- | ----------------------------------------------------------- | +| `POST` | `{BASE}/api/push/register` | `{"token":"","platform":"android"}` | +| `POST` | `{BASE}/api/push/unregister` | `{"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. | + +| 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` | + +| Method | Path | +| -------- | -------------------------------------- | +| `POST` | `{BASE}/wake_on_lan_host` | | `DELETE` | `{BASE}/wake_on_lan_host/:mac_address` | -| `PUT` | `{BASE}/wake_on_lan_host/:mac_address` | +| `PUT` | `{BASE}/wake_on_lan_host/:mac_address` | + ### HTML pages (session) @@ -178,11 +196,13 @@ These return HTML for the v2 shell, not JSON: `{BASE}/` (clients), `{BASE}/dashb ### 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. | + +| 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. | + --- @@ -211,46 +231,48 @@ 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 | + +| 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) @@ -260,35 +282,39 @@ The server can send **push notifications** (e.g. peer created/removed/enabled/di 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. +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: +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. | -| 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. +- `**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":"","platform":"android"}` | -| `POST` | `{BASE_PATH}/api/push/unregister` | `{"token":""}` | -Registered tokens are persisted under the server DB directory (e.g. **`push_tokens.json`** next to other JSON store files). +| Method | Path | Body | +| ------ | --------------------------------- | ----------------------------------------------------------- | +| `POST` | `{BASE_PATH}/api/push/register` | `{"token":"","platform":"android"}` | +| `POST` | `{BASE_PATH}/api/push/unregister` | `{"token":""}` | + + +Registered tokens are persisted under the server DB directory (e.g. `**push_tokens.json`** next to other JSON store files). #### Rate limiting @@ -296,72 +322,82 @@ Outbound FCM sends are **rate-limited per device token** (for example **at most #### 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. +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. | + +| 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. | `false` | -| `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`**. | + +| 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`**. +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) @@ -374,14 +410,14 @@ Use this when you want the **Logs** page to also show a custom application log f Environment="WGUI_LOG_TAIL_PATH=/var/log/wireguard-ui.log" ``` -2. Ensure file exists and is readable by the service user: +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 ``` -3. (Recommended) append service stdout/stderr to that file: +1. (Recommended) append service stdout/stderr to that file: ```ini [Service] @@ -389,14 +425,14 @@ StandardOutput=append:/var/log/wireguard-ui.log StandardError=append:/var/log/wireguard-ui.log ``` -4. Reload and restart: +1. Reload and restart: ```bash sudo systemctl daemon-reload sudo systemctl restart wireguard-ui ``` -5. Verify: +1. Verify: ```bash sudo systemctl show wireguard-ui -p Environment @@ -407,7 +443,7 @@ sudo tail -n 50 /var/log/wireguard-ui.log #### 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)**): +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] @@ -424,46 +460,51 @@ 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:///.well-known/assetlinks.json`** and checks package name + signing certificate fingerprints against your app install. +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:///.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:`**. 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. +Assertion signatures include `**Origin`** inside `**clientDataJSON`**. Browser users send origins like `**https://vpn.example.com`**. Native Android sends `**android:apk-key-hash:**`. 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). +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:///.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`**). | + +| Symptom | Typical cause | +| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `**RP ID cannot be validated`** (Credential Manager DOM error) | Missing or inaccessible `**https:///.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:///.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. +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:///.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. + +| 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** @@ -480,11 +521,11 @@ vpn.example.net { } ``` -Replace **`127.0.0.1:5000`** with your **`BIND_ADDRESS`** target (`docker` internal hostname, upstream socket, etc.). +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: +Enable `**mod_proxy**` + `**proxy_http**` (Debian/Ubuntu: `a2enmod proxy proxy_http`). Example: ```apache SSLProxyEngine on @@ -492,7 +533,7 @@ ProxyPass /.well-known/assetlinks.json http://127.0.0.1:5000/.well-known/ ProxyPassReverse /.well-known/assetlinks.json http://127.0.0.1:5000/.well-known/assetlinks.json ``` -Place specific statements **above** wildcard **`ProxyPass /`** directives. +Place specific statements **above** wildcard `**ProxyPass /`** directives. Equivalent **Nginx** pattern: @@ -504,7 +545,7 @@ location = /.well-known/assetlinks.json { } ``` -Verification (expect **`200`** and JSON body): +Verification (expect `**200**` and JSON body): ```bash curl -sSIL https://vpn.example.net/.well-known/assetlinks.json @@ -514,60 +555,48 @@ curl -sS https://vpn.example.net/.well-known/assetlinks.json **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`). +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. +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). +- `**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). +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://` 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 +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`). - + ``` + 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 + ```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 + ``` +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. @@ -581,53 +610,42 @@ service. Following is an example: ### 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. +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=`**. +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 +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`**. - + ``` + 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 + ```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 + ```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 +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`**: + ``` + 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] @@ -649,29 +667,26 @@ RestartSec=5 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. + 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. -6. **Reload systemd and start**: - - ```bash +1. **Reload systemd and start**: + ```bash sudo systemctl daemon-reload sudo systemctl enable --now wireguard-ui sudo systemctl status wireguard-ui - ``` - -7. **Logs**: - - ```bash + ``` +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. +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. +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] @@ -803,7 +818,7 @@ which you can pull and use ``` docker pull ngoduykhanh/wireguard-ui -```` +``` ### Build binary file @@ -825,6 +840,4 @@ MIT. See [LICENSE](https://github.com/ngoduykhanh/wireguard-ui/blob/master/LICEN ## Support -If you like the project and want to support it, you can *buy me a coffee* ☕ - -Buy Me A Coffee +If you like the project and want to support it, you can *buy me a coffee* ☕ \ No newline at end of file diff --git a/handler/passkeys.go b/handler/passkeys.go index 1bd1d10..f53780c 100644 --- a/handler/passkeys.go +++ b/handler/passkeys.go @@ -297,6 +297,9 @@ func setLoginSession(c echo.Context, dbuser model.User, rememberMe bool, session if ageMax <= 0 { ageMax = 86400 } + if isWireguardUiAndroidClient(c) && ageMax < androidCompanionIdleMinSeconds { + ageMax = androidCompanionIdleMinSeconds + } cookiePath := util.GetCookiePath() sess, _ := session.Get("session", c) diff --git a/handler/session.go b/handler/session.go index c261339..d47796a 100644 --- a/handler/session.go +++ b/handler/session.go @@ -15,6 +15,41 @@ import ( // WebAuthn credential ID (RawURLEncoded) used for the current login, if login was via passkey. const sessionPkLoginCredKey = "pk_login_cred" +// androidCompanionIdleMinSeconds is the minimum sliding idle window for requests that identify as the +// Flutter Android client (header X-WGUI-Client: android). Browser/HTML sessions keep using only +// global session_timeout_minutes from settings. +const androidCompanionIdleMinSeconds = 15 * 24 * 3600 + +const androidCompanionClientHeader = "X-WGUI-Client" +const androidCompanionClientValue = "android" + +func isWireguardUiAndroidClient(c echo.Context) bool { + return strings.EqualFold(strings.TrimSpace(c.Request().Header.Get(androidCompanionClientHeader)), androidCompanionClientValue) +} + +func effectiveIdleSeconds(sess *sessions.Session, c echo.Context) int64 { + maxAge := getMaxAge(sess) + if maxAge <= 0 { + maxAge = 86400 + } + v := int64(maxAge) + if isWireguardUiAndroidClient(c) && v < androidCompanionIdleMinSeconds { + return androidCompanionIdleMinSeconds + } + return v +} + +func effectiveIdleSecondsForCookie(sess *sessions.Session, c echo.Context) int { + maxAge := getMaxAge(sess) + if maxAge <= 0 { + maxAge = 86400 + } + if isWireguardUiAndroidClient(c) && maxAge < androidCompanionIdleMinSeconds { + return androidCompanionIdleMinSeconds + } + return maxAge +} + func sessionPasskeyCredentialID(sess *sessions.Session) string { if sess == nil { return "" @@ -76,13 +111,8 @@ func isValidSession(c echo.Context) bool { // Check time bounds createdAt := getCreatedAt(sess) updatedAt := getUpdatedAt(sess) - maxAge := getMaxAge(sess) - // Temporary session is considered valid within 24h if browser is not closed before - // This value is not saved and is used as virtual expiration - if maxAge == 0 { - maxAge = 86400 - } - expiration := updatedAt + int64(maxAge) + idleWindow := effectiveIdleSeconds(sess, c) + expiration := updatedAt + idleWindow now := time.Now().UTC().Unix() if updatedAt > now || expiration < now || createdAt+util.SessionMaxDuration < now { return false @@ -174,10 +204,7 @@ func touchSessionIdle(c echo.Context) { if err != nil || sess.Values["session_token"] != oldCookie.Value { return } - maxAge := getMaxAge(sess) - if maxAge <= 0 { - maxAge = 86400 - } + maxAge := effectiveIdleSecondsForCookie(sess, c) now := time.Now().UTC().Unix() sess.Values["updated_at"] = now