- TOTP (RFC 6238, stdlib-only) enrollment in profile, login step-up,
admin emergency reset.
- Admins can grant a user visibility into individual clients
(User.ClientIDs) in addition to whole-server access (User.ServerIDs).
- New "My Access" page: non-admin users see only their assigned clients
(view/QR/download only, no management), reachable from the main nav.
- GetUser/GetUsers now redact TOTPSecret before returning JSON.
No Go toolchain was available while writing this - not yet build-verified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvrfUytqd74H6WcQkRzFM4
Per-server Start/Stop/Restart via systemd wg-quick@<iface>.service units
plus live status badge, and a table/list-view toggle with checkbox
bulk-delete for both the server list and per-server client list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvrfUytqd74H6WcQkRzFM4
Redact the private key from the /servers/:id/keypair response body -
the UI never rendered it, but the raw key was still returned over the
wire (json:"private_key,omitempty" plus explicit clearing before the
JSON response).
Add a new import flow: an admin can upload an OPNsense config.xml,
preview the WireGuard servers/clients it defines (editable before
committing), and confirm to create the corresponding
Server/ServerSetting/Client records. Nothing is auto-applied - no
wg-quick/systemctl call happens, matching the existing manual "Apply"
step for regular server management.
Schema verified against OPNsense core (WireGuard has been in core
since 22.1, not a plugin) - see opnsense/parse.go for the confirmed
tag reference. Public keys are always re-derived from private keys
rather than trusted from the export; client public-key collisions
against existing store data are skipped and reported per-batch rather
than aborting the whole import.
Since OPNsense stores DNS/MTU per-server and keepalive per-client, but
this fork only had those app-wide (GlobalSetting), extended
ServerSetting with DNSServers/MTU and Client with PersistentKeepalive
as optional overrides that fall back to the global default when unset
- existing single-server behavior is unchanged when the override is
empty/zero. Manual UI editing of the per-client keepalive override
outside the import flow is left for a later pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VjwLYRA87o8m9a9zztgs3
SuggestIPAllocation always looked up the legacy "wg0" server regardless
of which server the request was for, so any setup without a migrated
wg0 (i.e. every fresh multi-server install) failed with
"open db/servers/wg0.json: no such file or directory" when adding a
new client.
Now accepts an optional server_id query param (falling back to wg0 for
the legacy bare routes), and the per-server clients page passes its
own server ID.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Escape HTML in client list and wake-on-LAN names to prevent XSS
- Log successful/failed login attempts with remote address
- Fix leading-comma bug in AllowedIPs template when only extra allowed IPs are set
- Add PreUp script support for server interfaces (alongside existing PostUp/PreDown/PostDown)
- Fix endpoint parsing to support IPv6 addresses (upstream PR #223)
Cherry-picked from upstream PRs #656, #653, #680, #673, #223.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New firewall.DisableGlobal() removes the wireguard_ui_global nftables
table without touching stored IP list entries, and firewall.IsGlobalEnabled()
reports whether it's currently loaded. New GET /firewall-lists/status and
POST /firewall-lists/disable endpoints (admin-only), plus a status badge
and "Toggle enable/disable" button on the Global Firewall Lists page -
one click to turn the whole thing off without losing the list contents,
and back on again (re-applies the current ruleset).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Paste-in import: one CIDR/IP per line, optional comment after '#',
blank/comment-only lines ignored - matches the format used by common
public blocklist feeds (Spamhaus DROP, blocklist.de, etc.) so those can
mostly be pasted in directly. Whole batch gets one list_type (allow or
block). Reuses the existing single-entry validation, skips duplicates
(by list_type+CIDR, including within the same paste), caps at 5000 lines,
and reports imported/skipped/invalid counts plus per-line errors.
New route: POST /firewall-lists/entries/import (admin-only). UI: a
collapsible "Bulk import" section on the Global Firewall Lists page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New model.IPListEntry + jsondb CRUD, independent of any single WireGuard
server. firewall.GenerateGlobalRuleset builds an nftables table
(wireguard_ui_global) with allow/block sets evaluated at priority -10 -
before every per-server table - so it applies to all traffic on the host,
not just WireGuard. Allow entries always win over block entries.
firewall.ApplyGlobal loads it live via `nft -f`, scoped to that one table.
New "Global Firewall Lists" page (nav entry under Settings): add/delete
entries, ruleset preview, "Apply now (live)" with an explicit confirm()
warning since this affects the whole host's firewall, not just one server.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New system package checks `apt list --upgradable` against the current
package index (no apt update triggered) and the reboot-required marker
file. Read-only - never installs or upgrades anything. Shown as a card
on the About page with a manual "Check now" refresh.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New model.FirewallRule + jsondb CRUD (GetFirewallRules/CreateFirewallRule/
UpdateFirewallRule/DeleteFirewallRule), scoped per server. firewall package
now generates a full ruleset (baseline + enabled custom rules) and can
apply it live via `nft -f` (firewall.Apply), scoped to a per-server
nftables table (wireguard_ui_<serverID>) so applying one server never
touches another server's rules or any pre-existing firewall state.
New endpoints: GET/POST /servers/:id/firewall/rules, POST .../rules/:ruleId,
POST .../rules/:ruleId/delete, POST .../apply (live, admin-only). UI in the
All Servers page: rule table with add/delete, ruleset preview, and an
"Apply now (live)" button with an explicit confirm() warning before it
touches the running firewall.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New firewall package generates an nftables snippet (INPUT accept for the
listen port, FORWARD rules for the WireGuard interface, optional LAN
forwarding via a new ServerSetting.LanInterface field). Text only -
nothing is applied to the live firewall. Exposed as GET
/servers/:id/firewall-preview and a "Firewall Preview" button in the
All Servers page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DeleteServer already existed in the store layer (refuses if clients
still reference the server) but was never wired up anywhere, so there
was no way to actually remove a server from the UI or API.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removed the release/contributor/author lookups against
ngoduykhanh/wireguard-ui - this fork no longer tracks that repo's
releases, so those calls were meaningless here and leaked an external
API dependency. Replaced with a short own-product description.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deletes /wg-server (route, handlers, template) entirely and adds generic
/servers/:id/interface and /servers/:id/keypair endpoints + UI in the
All Servers page, so every server (including the default one) is managed
through the same per-server registry. Drops the write-through dual-write
hacks that kept the old single-server collection in sync - the registry
is now the single source of truth. One-time legacy-install migration path
is untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No route/logic changes - pure label cleanup so the legacy default-server
editor and the multi-server management page are no longer confusable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New POST /backup/download (admin-only) tars the whole jsondb directory
(all servers/clients/users/settings) and streams it back as a file
download. If a passphrase is given, the archive is encrypted first
(AES-256-GCM, scrypt-derived key, backup/encrypt.go) - a small
self-contained format, not gpg/OpenPGP-compatible, to avoid shelling
out to an external binary or adding a PGP dependency.
Deliberately does NOT upload anywhere automatically (e.g. to
Nextcloud) - the archive only ever leaves the server as this one HTTP
response to the requesting admin, who is responsible for storing it
themselves. New "Download Backup" button + passphrase modal on the
Global Settings page.
New servers previously got a bare ConfigFilePath default with no way
to set/edit EndpointAddress, FirewallMark, or Table afterward. Adds
GET/POST /servers/:id/settings (view/save ServerSetting, admin-only
for writes) and a "Settings" button + modal on each server card in
servers.html. CreateServer now seeds FirewallMark/Table with the same
defaults the legacy single-server bootstrap uses, instead of leaving
them blank.
Real per-server data isolation, the core ask behind the access-control
work: clients, config generation, and the client-management UI are now
scoped by server ID instead of implicitly operating on one global
"the server".
- util.DefaultServerID ("wg0") is the server every legacy bare route
now resolves to, so old and new routes share one consistent identity
instead of drifting apart.
- New /servers/:id/... routes (new-client, update-client, remove-client,
set-status, download, api/clients, api/client/:cid, api/apply-wg-config)
reuse the same handlers as the legacy routes via resolveServerID(c),
gated by RequireServerAccess middleware. Cross-server edits/deletes on
scoped routes are rejected (403) if a client belongs to a different
server.
- Fixes a real data leak: ApplyServerConfig previously wrote ALL clients
from ALL servers into whichever single wg.conf it targeted. It now
filters clients by server ID before generating a config, and resolves
each server's own ConfigFilePath/EndpointAddress via the new
ServerSetting record instead of the app-wide GlobalSetting.
- WireGuardServerInterfaces/WireGuardServerKeyPair/GlobalSettingSubmit
(the legacy /wg-server and /global-settings edit routes) now write
through to the new per-server registry record for "wg0" in addition
to the legacy collection, so the two stay in sync until the legacy
routes are eventually retired.
- New templates/server_clients.html: per-server clone of clients.html
wired to the scoped endpoints, with a server name/id heading.
- base.html's shared "New Client" and "Apply Config" actions (used by
every page's nav buttons) now target the scoped route when a
serverID is present on the page, instead of always hitting the
legacy default-server endpoint regardless of which server's client
page is open.
Legacy bare routes (/, /new-client, /wg-server, ...) are untouched and
still fully functional against the default "wg0" server - nothing was
removed yet, per the incremental-delivery approach for this project.
obj.name is free text (unlike id/interface, which are already
regex-validated server-side) and was being template-literal'd
straight into innerHTML - stored-XSS risk if a server name contains
markup. Escape via jQuery .text()/.html() round-trip.
New admin-only page at /servers-settings (templates/servers.html) lists
servers and creates new ones via POST /servers (ID/name/interface/
addresses/port, key pair generated server-side). Nav gets a "Servers"
link.
templates/users_settings.html gains a multi-select "Server Access"
field wired to the server_ids support added to create-user/update-user
in the previous commit, so admins can now actually assign non-admin
users to specific servers through the UI.
The from-scratch Go rewrite had unresolved bugs (missing go.sum, UI
404s, path issues) from being built without a working local Go
toolchain to verify against. Switching strategy: use the actual
upstream wireguard-ui codebase (proven, battle-tested single-server
manager) as the base, and extend it for multi-server support instead
of re-deriving everything from zero.
Kept our own installers (bootstrap.sh, update.sh, scripts/install.sh,
scripts/proxmox-install.sh) - these still apply, just need updating
to build/install the upstream module layout instead of the old
cmd/wireguard-ui-multi structure.
Module path intentionally left as upstream's own
(github.com/ngoduykhanh/wireguard-ui) for now to avoid touching every
internal import; revisit if this needs to be fully rebranded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>