WireGuard server with an embedded admin console

Go backend that drives kernel WireGuard over netlink (wireguard-go as the
fallback), nftables NAT with MSS clamping, forwarding and buffer sysctls,
SQLite for peers, users, sessions, traffic history and the audit log.

React console: dashboard with live rates and usage history, peer management
with QR codes and .conf downloads, disconnect, session reset, key rotation,
expiry, client-supplied keys, settings, users with admin and viewer roles,
two-factor authentication with recovery codes, audit log.

Docker image on Alpine with compose files for bridged and host networking,
CI and GHCR publish workflows, performance notes.
This commit is contained in:
jcoffey
2026-09-12 19:56:08 -07:00
commit 6c006e1d4d
72 changed files with 11675 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
# WGX
A WireGuard server with a secure web console, in one container.
Start it, open the console, create a peer, scan the QR code. WGX runs the
tunnel on the kernel's WireGuard module, keeps the NAT rules and forwarding
sysctls in order, and gives you a dashboard that shows who is connected, how
much they are moving, and a button to cut them off.
## What it does
- **Peers.** Create, edit, disable, delete. The server generates the key
pair (and a preshared key) and shows a QR code and a `.conf` download; or
the client brings its own public key and the private key never leaves the
device. Pin a tunnel address or let WGX allocate one. Set an expiry and the
peer is disconnected on time. Rotate keys in one click.
- **Who is connected.** Live status from the interface counters every two
seconds: endpoint, last handshake, session length, current rate, total
transfer. Usage history in five-minute buckets, per peer and overall, kept
for 90 days.
- **Disconnect them.** *Disconnect* removes the peer from the interface and
keeps it off until you enable it again. *Reset session* drops the current
session and lets the client handshake afresh.
- **Fast.** Kernel data plane over netlink, no user-space hop. Tuned
sysctls, TCP MSS clamping, optional host networking. Falls back to
`wireguard-go` on hosts without the module and tells you so. See
[docs/performance.md](docs/performance.md).
- **Locked down.** argon2id passwords, two-factor authentication with
recovery codes, viewer and administrator roles, rate-limited login,
same-origin enforcement, strict CSP, built-in TLS if you want it, and an
audit log of every change (including every time a peer's configuration is
viewed). See [SECURITY.md](SECURITY.md).
- **Observable.** `/api/health` for a liveness probe and `/metrics` in
Prometheus format, guarded by a bearer token.
- **Self-contained.** One static Go binary, one SQLite file under `/data`,
no other services. Multi-arch image for amd64 and arm64.
## Quick start
```sh
curl -O https://raw.githubusercontent.com/Coffey-Labs/WGX/main/docker-compose.yml
# edit WGX_ENDPOINT (your public hostname or IP), then:
docker compose up -d
```
Open <http://localhost:51821>, create the first administrator, and add a
peer. Point the WireGuard app on your phone at the QR code.
The console is bound to localhost in the compose file on purpose. To reach
it from elsewhere, either set `WGX_TLS_SELF_SIGNED: "true"` and bind to the
address you need, or put a TLS-terminating reverse proxy in front of it and
list the proxy in `WGX_TRUSTED_PROXIES`.
For the fastest configuration, `docker-compose.host.yml` runs on the host
network; [docs/performance.md](docs/performance.md) says when that is worth
it and which host sysctls to set.
### Requirements
- Docker (or Podman) on a Linux host with a kernel from 5.6 on. Older kernels
work with `wireguard-dkms` installed on the host, or fall back to the
slower user-space data plane automatically.
- The container needs `NET_ADMIN` and the forwarding sysctls in the compose
file. `SYS_MODULE` is not needed unless the host has never loaded the
module and cannot autoload it.
- UDP port 51820 (or whatever you choose) reachable from the internet.
## Configuration
Infrastructure is configured through the environment; everything an
administrator might change while the server runs lives in the database and
is edited in the console under **Settings** (endpoint, DNS, default client
routes, MTU, keepalive, peer isolation, MSS clamping, preshared keys).
| Variable | Default | Meaning |
| --- | --- | --- |
| `WGX_ENDPOINT` | | Public hostname or IP for client configs. Also asked for at first-run setup. |
| `WGX_PORT` | `51820` | UDP listen port. |
| `WGX_SUBNET` | `10.8.0.0/24` | IPv4 tunnel network; the server takes the first address. |
| `WGX_SUBNET6` | | IPv6 tunnel network, e.g. `fd42:42:42::/64`. Off when empty. |
| `WGX_DNS` | `1.1.1.1, 1.0.0.1` | Resolvers handed to clients on first run. |
| `WGX_INTERFACE` | `wg0` | Interface name. |
| `WGX_EGRESS_INTERFACE` | auto | Interface to masquerade on. Auto uses the default route. |
| `WGX_HTTP_LISTEN` | `:51821` | Console listen address. |
| `WGX_TLS_SELF_SIGNED` | `false` | Serve HTTPS with a certificate generated into `/data`. |
| `WGX_TLS_CERT`, `WGX_TLS_KEY` | | Serve HTTPS with your own certificate. |
| `WGX_SECURE_COOKIES` | `false` | Mark cookies `Secure` when TLS terminates at a proxy. |
| `WGX_TRUSTED_PROXIES` | | CIDRs whose `X-Forwarded-For` is believed. |
| `WGX_METRICS_TOKEN` | | Bearer token for `/metrics`. A signed-in session works too. |
| `WGX_SESSION_IDLE` | `12h` | Sign out after this much inactivity. |
| `WGX_SESSION_MAX` | `168h` | Sign out after this long regardless. |
| `WGX_TRAFFIC_RETENTION` | `2160h` | How long usage history is kept (90 days). |
| `WGX_POLL_INTERVAL` | `2s` | How often the interface counters are read. |
| `WGX_BACKEND` | `auto` | `kernel`, `userspace` or `mock`. Auto prefers the kernel. |
| `WGX_MANAGE_FIREWALL` | `true` | Set to `false` if the host owns the NAT rules. |
| `WGX_MANAGE_SYSCTL` | `true` | Set to `false` if the host has tuned itself. |
| `WGX_DATA_DIR` | `/data` | Where the database and TLS files live. |
| `WGX_LOG_LEVEL`, `WGX_LOG_JSON` | `info`, `false` | Logging. |
## Locked out?
```sh
docker exec -it wgx wgx reset-password admin
```
sets a new password for that user, clears their second factor and ends
their sessions. It runs against the same database, so no restart is needed.
## Client setup
Any WireGuard client works: the official apps on iOS, Android, macOS and
Windows, `wg-quick` on Linux, and routers that speak WireGuard. Scan the QR
code from the peer's **Configuration** tab, or download the `.conf`. The
default configuration routes everything through the tunnel; change **Client
routes** on the peer (or the default under Settings) to the tunnel subnet
alone for split tunnelling.
## API
Everything the console does goes through `/api/…` with the session cookie.
`GET /api/peers`, `POST /api/peers`, `GET /api/peers/{id}/config`,
`POST /api/peers/{id}/disable` and friends are stable enough to script
against; the shapes are in `internal/server/api.go`. A cross-site request
without a same-origin `Sec-Fetch-Site` or `Origin` header is refused, so
call it from the same origin or from a non-browser client.
## Building from source
```sh
cd web && npm ci && npm run build && cd ..
go build ./cmd/wgx
```
The UI is embedded in the binary. `docker build -t wgx .` does both steps.
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development loop against the
mock data plane, which needs no privileges.
## Licence
AGPL-3.0-or-later. See [LICENSE](LICENSE).