Status, priority/torrent chips and IP columns like the users tab; the API
returns each device's IP per server. The key row sums its devices (online,
priority, traffic); key UUID and device ID sit under the name, copy on click.
- awg-ctrl: optional nft table awg_torrent (GET/PUT /api/torrent, off by
default). A connection is judged by its first packets, like Xray's
sniffing: DHT, UDP trackers, uTP SYN, and with @ih support the plain
TCP handshake and HTTP announce. A match marks the connection in
conntrack and drops it whole; without @ih only the UDP part is used
- devices that tried are kept in a 10-minute set and flagged as
"torrent" in user and device stats; with shaping on they sit in the
background class while the flag lasts
- metrics: awg_torrent_block_enabled/active, awg_torrent_blocked_total,
awg_torrent_devices and per-peer awg_peer_torrent
- panel: "Торренты" section in server settings, "торренты" chip on users
and devices; Save sends only what changed
- ranges 7d (30 min step) and 30d (2 h step); Prometheus keeps 31d
- rate() window is at least the step, so long views show averages
instead of a 2-minute sample; "now" values still use a 2m window
- overview returns used_down/used_up: increase() of the interface
counters over the whole range
- tiles merged: traffic (with volume for the period), devices, load
Baseline TypeScript version before the Go rewrite: CAKE fair sharing with
automatic deprioritisation of heavy devices, per-key priority, /metrics with
a metrics token, the built-in Prometheus install/remove, the Monitor tab,
grouped sidebar tabs and the dead-peer audit.
Lowering device_limit below the number of bound devices now forgets the
extra ones right away, newest first, so the devices that took their
slots first keep them. Their peers are removed and they get 410 like a
revoke from the panel. PATCH returns the unbound ids; the panel names
the devices that will be dropped and asks before saving.
- master_keys.uuid (UUID v4, unique index): new keys get one on create,
existing keys once on start. It survives rotate (new secret) and
relabel, so it identifies the key itself rather than its link.
- /ui/masterkeys* and /api/v1/masterkeys* return uuid, and :id in those
paths accepts the numeric id or the uuid.
- /sub/v1 register, peek and devices return key_uuid, so a client can
recognise the same key behind a reissued link. It is not in the sen://
link (that would need a new format) and not in config, so rev and
existing clients are unaffected - checked against a 0.3.1 ui.db.
- Panel: the uuid sits in the key's row like a device ID (full value in
the tooltip, click copies) and in the key settings with a copy button;
on mobile it is shown under the key's name.
- README: uuid in the master key API.
API keys now see a set of servers instead of one, and /api/v1 gains the
master-key operations the panel already had.
- api_key_servers holds each key's servers (migrated from server_id,
which stays the default server for /api/v1/users). /users follows
X-Server-Id within the set; a server outside it is 404. PATCH
/ui/apikeys/:id changes the set without reissuing the key.
- /api/v1/masterkeys: create (returns the sen:// link), list, get with
link and devices, patch label/limit/servers, rotate, rekey, delete, and
nested device routes (unlink, rekey, PSK). A key sees a master key only
if all of its servers are in the key's set, so it cannot delete or
re-point service on a server it cannot see; anything else is 404,
servers outside the set are 403. Creating refuses with 503 before
writing anything if the subscription cannot issue links.
- sub.ts: the panel routes and the API share one service layer, the
panel's behaviour is unchanged.
- Node deletion: owners check in beforeDelete (no writes, a later hook
may still veto) and clean up in afterDelete. Previously a forced delete
could release a waiting master key and then be vetoed. A node that is
the only server of an API key is refused; other keys just lose it.
- Panel: server checkboxes are a shared ServerPicker, used by the master
key settings and the new API key settings row; the API keys table shows
each key's servers.
- README: the API section covers server sets and master keys.
A deleted master key could keep letting its devices in: ui.db learns what
is on a server only from awg-ctrl's reply, and a node that executed a
request whose 504/502 reply got lost, or a device deleted while a
reconcile pass was in flight, left a peer nothing remembered to remove.
- awg-ctrl: PUT /api/peers/sync takes a panel's full list of wanted keys
and removes every other peer of that panel's area. Removal is driven
only by such an explicit list, never by time or inactivity: a panel
that is offline sends nothing, so nothing is deleted. Registered before
PUT /api/peers/:pub, with its own 512 kB body limit.
- Every panel has a stable id (ui.db, mirrored to awg-ui/panel.id) and
adds peers as "<panel>/m<key>/d<device>", so two panels on one awg-ctrl
never touch each other's peers. Legacy "m1/d2" peers are claimed only
when their key is in the panel's list.
- Each reconcile pass ends with the sync, built from ui.db at that moment.
Its reply also self-heals: a device whose key is missing on a server
(a rekey rolled back after a lost reply) is added again.
- Deleting a master key stops it at once but keeps it listed as
"Удаляется — ждёт: …" until every server confirms, instead of saying
"deleted" while an offline node still lets the devices in.
- Deleting a node reconciles it first; if it is offline or has not
confirmed a deleted key, it takes ?force=1 and a warning. Tombstones
are no longer dropped silently.
- /sub/v1: an unknown device gets a signed 410 revoked that does not count
towards the failure limit (dead clients behind one node NAT used to
lock everyone out), every response echoes the request's ts and device,
and 401 reasons and the first 429 per window are logged. config and rev
are unchanged, byte-identical on one server.
- CLI: `awg-ctrl peers [drop <area|legacy>]` lists and removes
subscription peers by area; `join --no-panel` asks whether to keep or
drop the panel's own peers (NODE_PEERS=keep|drop).
A master key now has a set of servers (the panel's own and/or nodes). A
device's single WireGuard key becomes a peer on every server of its key,
each with its own IP and PSK, and config.servers[] lists them all.
- ui.db: master_servers (the key's set, never empty), device_peers (which
key is on which server, with the ip/psk that server issued),
peer_tombstones (peers to delete) and server_profiles (last known
profile of each server).
- Nothing is pushed blindly: reconcile() brings one server to the desired
state - deletes tombstones, adds peers, rekeys with PUT, fills ip/psk,
rotates a pending PSK - one pass per server at a time. It runs right
after every action, when a node connects and once a minute, so a node
that was offline catches up on rekeys, PSK changes and removals.
- config is built from ui.db, so an offline node stays in it and rev does
not flap; a server is listed once its peer carries the device's current
key. An empty config is never sent (502 instead); register and rekey
roll back if no server accepted the key.
- Existing keys and devices migrate on start. With one server the config
and its rev are byte-identical to 0.3.1 (checked by running both
versions on the same ui.db), so existing clients notice nothing.
- Panel: server checkboxes in the key settings instead of the placeholder
(the last one cannot be removed), "на N из M серверов" on devices that
have not reached every server yet, device stats summed over servers.
- SUB_HOST: the panel's public address for sen:// links and endpoints;
install.sh writes it. Without it a panel with no VPN of its own used
the address it was opened at.
- A node referenced by a key's server set cannot be deleted.
A node is a VPN server without a panel. awg-agent on it dials the panel
(core) over an outbound WSS connection and runs the panel's requests on its
own awg-ctrl, which stays on 127.0.0.1 and is unchanged. No inbound port on
the node, works behind NAT.
- awg-ui/nodes.ts: node registry in ui.db, WSS hub on NODE_PORT with a
self-signed cert whose SPKI pin goes into the join string, RPC with
timeouts, health polling, /ui/nodes routes.
- Nodes enroll with a one-time awgjoin:// string (secret stored hashed,
1 hour) and then authenticate by their own Ed25519 key. The agent checks
the pin before sending anything and only forwards /health, /api/*, /awg/*.
- server_id 0 is the panel's own awg-ctrl; LOCAL_NODE=off makes the panel
run without a VPN of its own. The panel picks a server with X-Server-Id,
API keys use their server_id. sen:// still serves one server.
- UI: a card per server in the server bar, switching remounts the tab,
«Добавить сервер» gives the join command.
- CLI: agent service, roles from cli.env, join / unjoin commands.
- Release archives include awg-agent.
A master key is a sen:// link with a device limit. Each device
generates its own WireGuard keypair and registers only the public key,
then polls a signed /sub/v1/config, so params and generation changes
reach users without re-importing a key.
- awg-ctrl: peers table and /api/peers*, /api/profile; nextIp and
rebuildConf take peers into account (output unchanged without them)
- awg-ui: link codec and signatures (sen.ts), registry, panel routes
and the public listener on SUB_PORT (sub.ts)
- responses are always Ed25519-signed; TLS with a pinned self-signed
certificate is optional
- devices report a client version, shown as platform-version
- "Мастер-ключи" tab: a table with the devices nested under each key,
a device-limit meter, per-key settings and a placeholder for the
servers a key serves; cards on mobile
- install.sh/CLI: subscription signing key, optional certificate, port;
kept across reinstalls with KEEP_DATA
- add the missing mock-server.ts used by npm run dev:mock
Commit 0dbb13f removed the whole feature: the apikeys tab, the /ui/apikeys
routes, requireApiKey, the /api/v1 router, the ApiKey type, the better-sqlite3
dependency and UI_DB_FILE in the CLI and the installer. Only the untracked
awg-ui/API.md survived, so the README went on to claim there was no key-based
API at all.
Restores all of it: ui.db with the api_keys table, awgk_ key generation storing
only the SHA-256 and a display prefix, the JWT-guarded CRUD, and the /api/v1
router mounted before the JWT proxy on /api so the key auth is not swallowed.
The tab comes back as its own folder and the registry picks it up on its own.
UI_DB_FILE points at /etc/amnezia/amneziawg/ui.db, outside the install tree, so
reinstalling keeps the keys; a fresh install moves the old one aside with its
WAL sidecars.
better-sqlite3 takes the slot of jsonwebtoken, which arrived with the MA7
integration and has been unused since 068cab4 removed it.
One defect is not reproduced: the /api/v1 handlers called ctrl() without a
try/catch, and Express 4 does not catch async rejections, so a single request
with awg-ctrl down killed the panel process. They now answer 502 like proxy()
already did.
The API contract is documented in the README rather than linked: .gitignore
excludes every markdown but README.md, so awg-ui/API.md is not in the repo and
the link would break on a clone.
Checked against a running server: login, key create/list/delete, 400 on a bad
label, 401 without a JWT, "API key required" rather than "Unauthorized" on
/api/v1, "Invalid API key" for a deleted key, last_used updating, and all three
/api/v1 routes returning 502 with the process still alive.
install.sh now defaults to AWG_GEN=3.1: an incompatible module or kernel
fails with diagnostics instead of silently falling back to 2.0 and handing
out a server with weaker obfuscation. AWG_GEN=auto keeps the old fallback
for old kernels, AWG_GEN=2.0 still forces 2.0.
Servers already installed on 2.0 no longer need a reinstall: POST
/awg/upgrade (button in the users tab) backs up awg1.conf, rewrites
[Interface] with HeaderProtectionKey and the 3.1 block install.sh writes,
raises any S1-S4 below 12 (header protection needs a 12-byte nonce and the
module only reports "Invalid argument"), restarts the interface and
reissues every vpn:// key. Bringing the interface up is the only real proof
the kernel accepted the params, so a failure restores the previous conf and
brings 2.0 back up. Reissue is not optional: 2.0-era keys stop working the
moment the generation changes.
restartAwg() now re-reads the obfuscation params, generation and keepalive
from awg1.conf before syncing peers. The conf can change while the
interface is down, and a stale CONFIG.gen meant new keys were built on the
old params.
README: install from git.ma7neko.ru (Gitea) instead of raw.githubusercontent,
and document the 3.1 default with the AWG_GEN=auto escape hatch.
Upstream AmneziaWG 3.x is still shifting under active development
(unanswered kernel-module issues around header protection and
handshake-without-data regressions), so give operators an explicit
opt-out/opt-in instead of always auto-selecting 3.1, and pin the
installed package so unattended-upgrades can't swap the module under
a live server. Surface the running module/tools version and protocol
generation through /health, the CLI status command, and the panel's
server card so a mismatch is visible instead of silent.
Protocol generation is now derived from awg1.conf rather than stored: an
interface speaks 3.1 when HeaderProtectionKey is set and 2.0 otherwise. The
3.0/3.1-only keys (HeaderProtectionKey, ContentPaddingAddition, the
Rekey/Reject/Keepalive timers, MaxHandshakeAttempts, RandomTrailers,
DisableCookies) are read from the conf and emitted only when non-empty, so 2.0
output stays byte-identical — verified by diffing both the client .conf and the
full vpn:// string against the previous implementation.
Changing generation invalidates every issued vpn:// key, so add
POST /api/users/reissue. It recovers the client private key from inside the
stored blob (the only place it exists), keeping ip/pub_key/psk_key intact, so
nothing changes on the wire and users only need to re-import. users gains
key_gen and vpn_key_prev, and users.db is snapshotted before the pass.
install.sh gates 3.1 on module 3.x and kernel >= 5.5 (header protection needs
the chacha library API, absent before 5.5) and falls back to writing a 2.0 conf
instead of aborting, so older kernels keep working as before.
HeaderProtectionKey is generated with awg genpsk and preserved on KEEP_DATA.
The panel marks keys issued on a different generation and offers to reissue
them; /api/v1 now returns gen alongside name/ip/vpn_key.
Also add MIT license headers across the awg-ui sources.