- New in-memory login throttle (handler/login_throttle.go): 5 failed
attempts per IP or per username within 5 minutes locks that key out for
5 minutes, applied to both /login and the TOTP verification step, which
previously had no rate limiting at all
- router.New now adds middleware.Secure with X-Frame-Options,
X-Content-Type-Options, Referrer-Policy, and HSTS (only when cookies
are Secure, implying an HTTPS deployment). No CSP: the existing
templates rely on inline <script> blocks, so a CSP strict enough to
matter would need 'unsafe-inline' anyway
- All session/auth cookies now set Secure based on the new
--cookie-secure flag / WGUI_COOKIE_SECURE env var (default true) so the
session cookie is never sent over plain HTTP unless explicitly opted
into an HTTP-only LAN deployment
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATVUwTa4Pqwq26orW5BcDW
A client CIDR with nonzero host bits (e.g. 10.66.120.1/30 instead of the
network 10.66.120.0/30) is tolerated by wg's own AllowedIPs/setconf, which
just warns, but plain iproute2 rejects it with "Invalid prefix for given
prefix length" and aborts the whole PostUp chain, taking the interface
down on start. Added a routeNet template func that clears host bits via
net.ParseCIDR before generating each PostUp route line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ATVUwTa4Pqwq26orW5BcDW
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
GetClients, GetClientByID, and SendRequestedConfigsToTelegram all
looked up the legacy "wg0" server unconditionally when building a
client's QR code / config, ignoring which server the client actually
belongs to. On any multi-server setup without a migrated wg0, the
server lookup returned a zero-value model.Server (error discarded),
and BuildClientConfig then dereferenced its nil KeyPair pointer,
panicking whenever a client's QR code was rendered.
Now looks up the client's own ServerID (falling back to wg0 only for
legacy clients with no ServerID set) and surfaces the lookup error
instead of silently continuing with an empty server.
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>
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>
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.
New IStore methods for a per-server registry (GetServers, GetServerByID,
CreateServer, DeleteServer, GetServerSettings/SaveServerSettings,
GetServerHashes/SaveServerHashes), all additive - existing single-server
methods untouched. Adds util.ValidateRecordID/ValidateInterfaceName and
applies them to every new method taking a server ID, closing a path-
traversal gap before serverID is ever driven by user input (flagged by
a security review pass).
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>