The torrent flag was read inside loops over users rows (users/stats,
peers, metrics) and each read queried the config table. SQLite has a
single connection here, so the nested query waited for it forever: the
first stats request or Prometheus scrape locked awg-ctrl for good, and
the panel showed no users and timed out.
- the block setting is kept in memory (loaded at start, updated on
save), so the flag never touches the database
- the set of flagged IPs is taken once before the query, not per row
- regression test: collectMetrics must finish with users in the table
Panel: a 401 from the panel itself (its JWT not accepted) now returns to
the login screen; a 401 relayed from a node's awg-ctrl does not.
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.
- awg-agent sends its install's VERSION (from the CLI's env, else the
project's .env) in the auth frame; the hub keeps it in nodes.version
(added by migration), so an offline node still shows the last one.
- GET /ui/nodes returns version per server (0 is the panel's own) and the
panel's version at the top.
- ServerBar: a vX.Y.Z chip next to the AWG generation. A node older or
newer than the panel, or one that sends no version (agent before
0.3.4, shown as v?.?.?), gets a warm chip with an "update it" tooltip.
- App.css: tertiary-container tokens from design.md (light) and a warm
dark pair; they did not exist yet.
- Mock server: panel 0.3.3, Frankfurt behind on 0.3.2, Amsterdam with no
version.
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 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.
The product name, the channel word "Alpha" and the version were scattered
across install.sh, the CLI banner, two spots in App.tsx and three
package.json files, so a rename or a bump meant editing strings in code.
They now live in one committed .env at the project root (BRAND, CHANNEL,
VERSION), shipped inside the release archive and read at runtime: the CLI
loads it after cli.env — so real env and machine settings still win — and
injects the three values into awg-ui, which serves them from GET /ui/brand
for the wordmark. awg-ui also parses the same file directly, so
`npm run server` in dev shows what production shows.
install.sh keeps its own VERSION and CHANNEL and says why: it prints the
banner and builds the release URL before the archive holding .env has been
downloaded. It writes BRAND into cli.env only when the operator passed one
explicitly, so the file stays the single source otherwise.
Version bumped to Beta 0.2.0 across .env, install.sh and the three
package.json files.
Nothing about the install was overridable: the archive could only come from
one GitHub repo, the connectivity precheck pinged github.com whatever the
source was, and the product name was duplicated across install.sh, the CLI
banner and two spots in App.tsx.
install.sh now reads REPO_BASE (default the Gitea mirror), REPO_FALLBACK
(default GitHub), ARCHIVE_URL and BRAND from the environment. Gitea and
GitHub share the same release path, so one scheme covers both:
download_extract tries the primary, warns and falls back to the secondary,
and fails with both URLs listed. The fallback carries real weight today —
the Gitea repo is a pull mirror that does not sync releases, so artifacts
live only on GitHub until release sync is enabled there. The precheck now
probes the hosts actually used.
BRAND flows install.sh -> cli.env -> CLI banner and awg-ui, which exposes
it via GET /ui/brand along with the package version. That route is
unauthenticated because the wordmark is on the login screen, before a JWT
exists; the SPA falls back to a constant if it fails. Renaming a fork is
one environment variable, and no version string is hardcoded in the UI or
the CLI any more.
Also adds /ui to the Vite dev proxy (GET /ui/brand and the API-keys tab
were unreachable in dev), documents the overrides in the README, fixes a
typo in the API.md base URL and states there that the API is not bound to
any host.
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.
Three defects surfaced on a real upgrade from 2.0.
The module gate read modinfo, which reports the version on disk. apt
--only-upgrade rebuilds the DKMS module but does not replace the one already
loaded, so the installer wrote a 3.1 conf that the running 2.0 module rejected
with 'Unable to modify interface: Invalid argument'. It now compares
/sys/module/amneziawg/version, reloads the module when it lags behind the disk,
and decides the generation from the loaded version only.
Bringing the interface up is the only real proof the kernel accepted the 3.1
parameters, so a failure there no longer aborts the install: it enables dynamic
debug, retries, prints the amneziawg lines from dmesg, then strips the 3.x keys
and comes up on 2.0. The rejected conf is kept as awg1.conf.awg31.
PostUp appends 'iptables -A FORWARD', but UFW inserts its jumps at the head of
the chain, so client traffic was dropped with '[UFW BLOCK] IN=awg1 OUT=eth0' —
the tunnel connected and had no internet. When UFW is active the installer now
adds 'ufw route allow in on awg1 out on eth0' and opens the AWG and UI ports.
Version 0.1.4.1.
install.sh drives the release URL and tarball name off VERSION, so the
matching tag is v0.1.4. awg-ctrl/package.json was still on the default 1.0.0
and is now in step with cli and awg-ui.
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.