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.
- 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
- version 0.4.0 in install.sh, .env and the service package.json files
- the installer and its release archives come from GitHub by default;
REPO_BASE / REPO_FALLBACK / ARCHIVE_URL still override the source
- README: install commands point to raw.githubusercontent.com
Releases now carry one archive per architecture with a static awg-control
binary (services and CLI), the built frontend and .env; the installer no
longer installs Node.js or runs npm.
- tools/release/package.sh: CGO-free builds for linux/amd64 and arm64,
version injected into `awg-control version`; used by both the GitHub and
Gitea release workflows
- install.sh: architecture check, per-arch archive, /usr/local/bin/awg-ctrl
is a symlink to the binary, systemd unit without node in PATH, steps
renumbered 1/6..6/6
- update from a Node install: the old services are stopped through the old
CLI, then TS sources, node_modules and the old dist are removed; cli.env,
panel.id and logs stay
- README: one binary, its roles, architecture requirement
internal/cli reimplements cli/src: start/stop/restart/status with the
awg-ctrl health gate, credentials, join/unjoin, peers [drop], monitor
install/status/remove and the interactive menu. Same cli.env, pid files,
logs and messages. Without arguments or with an operator command the binary
acts as the CLI; ctrl/ui/agent stay service roles.
- services are started as the same binary (`awg-control ctrl|ui|agent`),
detached into their own session, from the same working directories
- processes started by the Node CLI (tsx … index.ts / server.ts) are still
recognised, so `stop` and `status` work across the switch
- after join the fresh restart gets the original environment, so a rejoin
picks up the new cli.env instead of the inherited old CORE_* values
- install root: AWG_ROOT, else the nearest parent of the binary with cli/ or
.env, else /opt/awg-control
Checked end to end on stubbed awg: start/status/stop, credentials, join to
the panel's own hub (with and without --no-panel), peers listing and drop,
unjoin, menu.
internal/ui reimplements the awg-ui backend with the same HTTP surface; the
React frontend is unchanged and served from awg-ui/dist.
- web.go: Express-like router (express.json semantics, mounts, :params,
HTML 400/404/413 pages, SPA fallback)
- server.go: JWT login with rate limit, API keys and their server sets,
awg-ctrl client and proxy, /api/v1/users, delete hooks
- nodes.go: WSS hub for agents (pin, challenge-response, join strings),
RPC with timeouts, health, /ui/nodes
- sub.go, audit.go, sub_admin.go: sen:// master keys and devices,
per-server reconcile with tombstones and /api/peers/sync, signed /sub/v1
listener, audit with two-pass confirmation and cleaning, panel and
/api/v1/masterkeys routers
- metrics.go, monitor.go: /metrics exposition with tokens, /ui/monitor
- sen.go: link codec and protocol signatures, tested byte for byte against
fixtures produced by awg-ui/sen.ts (tools/golden/sen.ts)
Differential bench (tools/diffbench/local-ui.sh, scenario-ui.ts,
compare-ui.mjs): 158 steps over every route group, including signed device
requests and a Go agent joining as a node, plus the resulting ui.db and
awg-ctrl state. TS and Go are identical after masking random values, across
repeated runs and with the race detector on.
The differential bench (GEN 2 and 3.1, with I1, S4=0, upgrade with a 3.x
module) now shows the Go awg-ctrl identical to the TS one. The only real
difference it found: host.cpu was a Go map, so its keys came out sorted
instead of in /proc/stat order.
- tools/diffbench/compare.mjs: decodes vpn:// (Node and Go zlib differ), masks
live host numbers, Express dev-mode stack traces, temp file names and the
order of calls from concurrent background work
- tools/diffbench/local.sh: the same run without Docker, stubs only on the
run's own PATH (for disposable machines)
- run.sh: wait for the background shaping apply before stopping the service,
otherwise the stub log is cut short
- internal/agent: outbound WSS to core with SPKI pin checked inside the TLS
handshake, Ed25519 challenge-response, one-shot join secret (dropped from
memory and cli.env after the first ready), forwarding of core requests to the
local awg-ctrl with an internal token, dead-connection timer on pings,
backoff up to a minute (a minute after 4003)
- path filter is stricter than the Node one where it relied on WHATWG URL
normalisation: backslashes, a leading "//" and %2e dot segments are rejected
- tests: end-to-end against a fake TLS core and a fake awg-ctrl; also checked
by hand against the real awg-ui/nodes.ts hub
- CI: Go build, vet and race tests on GitHub and Gitea
- internal/jsjson: JSON.stringify-compatible encoder and order-preserving decoder,
tested against fixtures produced by Node itself
- internal/ctrl: awg-ctrl in Go (params, vpn:// keys, users, peers, sync, 3.1
upgrade, CAKE shaping, metrics, monitor job, Express-compatible HTTP layer)
- internal/iauth, internal/logx: internal Ed25519 tokens and logging
- tools/diffbench: Docker bench with stubbed awg/ip/tc/nft that runs one scenario
against the TS and Go implementations for comparison
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.
All seven places the version is written down: .env, the copy in
install.sh, the four package.json files and the /ui/brand example in the
README.
The panel shows each server's Forgetting version; master keys get a
stable uuid; lowering a key's device limit unbinds the newest devices.
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.
- 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.
All seven places the version is written down: .env, the copy in
install.sh, the four package.json files and the /ui/brand example in the
README.
Deleted devices get a signed 410 revoked with an echo of the request, so
a client can tell a revoked key from a network failure; master-key peers
no longer outlive their devices; API keys can manage master keys.
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).
All seven places the version is written down: .env, the copy in
install.sh, the four package.json files and the /ui/brand example in the
README.
A master key can serve its devices from several servers; a server that
joins a core as a node keeps its own panel.
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 server that already works as panel + VPN can now also be a node of
another panel without losing its own: `awg-ctrl join` keeps awg-ui running
next to the agent and writes NODE_PANEL=on. `--no-panel` stops it as
before. Both panels talk to the same awg-ctrl, which stays the source of
truth, so they do not get in each other's way.
The CLI runs awg-ctrl, awg-ui and awg-agent on such a node, all behind the
awg-ctrl health gate; a node installed by install.sh has no UI_PASS and
still runs only awg-ctrl + agent. unjoin also drops NODE_PANEL.
Covers all seven places the version is written down: .env, the copy
install.sh keeps because it prints its banner and builds the release URL
before the archive with .env exists, the four package.json files (awg-agent
is new), and the /ui/brand example in the README.
Multiple servers under one panel (core + nodes), the installer's role
question and its full log.
Found by running install.sh end to end in a sandbox for all three roles.
- The suggested UI password and JWT_SECRET came from
`tr < /dev/urandom | head -c N || openssl rand`. Under pipefail tr's
SIGPIPE made the pipeline fail every time, so openssl ran too and the two
outputs were glued together (a 24-character password). rand_alnum() reads
a finite 1 KiB instead.
- The log filter runs sed with LC_ALL=C, so broken UTF-8 in someone else's
output (make.log, dmesg) cannot stop it, and tee -p keeps writing to the
screen if the log writer dies instead of taking the installer down with a
silent SIGPIPE.
- Output: labels before awg-quick up and apt-mark hold, no double blank
lines, "маршрут" only where there is a VPN, a hint when the agent log is
still empty.
- install.sh asks the role first: panel + VPN (as before), panel only
(skips AmneziaWG, the kernel module and the headers/virt checks) or node
(no panel; takes the awgjoin:// string and checks the panel's cert pin
with openssl before building anything). ROLE= / JOIN= skip the questions;
an update keeps the previous role.
- The previous cli.env is read before a reinstall removes the project
directory. Before this, keeping data silently lost SUB_PORT / SUB_TLS
and broke every issued sen:// link.
- Output: a header, numbered steps, menus, a summary before installing and
a final block per role. The full output of apt, DKMS, npm, awg-quick,
ufw and systemctl is shown as a dimmed block under its step, including
what used to go to /dev/null.
- The log starts with the first line: checks, questions with answers
(password masked), all command output. It is written without colour
codes, mode 600.
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.
- GET /sub/v1/devices: a device lists the devices of its own master key
(name, platform, version, last seen, and which one is asking). Read-only:
unbinding is still only DELETE /sub/v1/device for oneself.
- POST /sub/v1/peek: how many slots of a key are taken, asked with the
link's secret before any device exists, so the client can show it while
the link is only pasted. Wrong secrets count against the failure limit.
- register also answers with devices / device_limit, so the client needs no
second request to show the slot it just took.
Covers all six places the version is written down: .env, the copy install.sh
keeps because it prints its banner and builds the release URL before the
archive with .env exists, the three package.json files, and the /ui/brand
example in the README.
First release with the sen:// subscription and master keys.
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
Covers all six places the version is written down: .env, the copy install.sh
keeps because it prints its banner and builds the release URL before the
archive with .env exists, the three package.json files, and the /ui/brand
example in the README.
install.sh only knew Ubuntu. On Debian 13 the dependency step died on
software-properties-common, which trixie no longer ships, and on
linux-headers-generic, which Debian never had — so add-apt-repository and the
Ubuntu headers meta package are both off the table there.
The distro family now comes from /etc/os-release (derivatives via ID_LIKE) and
shows up as its own line in the compatibility checklist. On Debian the amnezia
PPA is wired by hand: the key is fetched from keyserver.ubuntu.com by its full
fingerprint and checked after download, then a signed-by source is written on
the Ubuntu series whose glibc is not newer than the Debian release's
(bullseye→focal, bookworm→jammy, trixie→noble; AMNEZIA_PPA_SUITE overrides).
Headers are installed as a separate step so a missing package gets precise
advice instead of a generic failure. Debian's archive keeps only the latest
kernel build, so a server that hasn't rebooted in a while has no headers for
its running kernel; the hint names linux-image/linux-headers for the kernel's
own flavour (cloud-amd64 and so on) plus a reboot.
iptables joins the dependencies — PostUp calls it, and a minimal Debian 13 on
nftables doesn't have it. IP forwarding moves to its own file in
/etc/sysctl.d: Debian 13 no longer reads /etc/sysctl.conf at boot, so the old
line would have held only until the first reboot.
Covers all six places the version is written down: .env, the copy install.sh
keeps because it prints its banner and builds the release URL before the
archive with .env exists, the three package.json files, and the /ui/brand
example in the README.
A self-contained template: an Express backend with login, JWT, rate limiting
and SPA serving, and a React 18 + Vite shell with the same tab registry and
warm Material 3 design system the panel uses. No domain logic — routes and tabs
are meant to be hung off it.
node_modules and the frontend build stay out through the existing ignores.
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.
The public /api/v1 surface and its docs went away with the API keys, leaving
nothing written down about what the panel exposes — even though every route it
uses is reachable over HTTP with a login token.
Replaces the install-source section with an API chapter: the JWT flow and its
401/429 cases, the unauthenticated /ui/brand, health and interface status, the
user routes including why fetching a key is a POST, reissue, and the 2.0 to 3.1
upgrade with its 409 and 500 cases. What is left of the install-source notes is
one line pointing at the comments in install.sh and .env.
Shapes and status codes are taken from the handlers rather than from memory,
and the auth behaviour was checked against a running server.
The integration was half-committed: server.ts imported ./ma7 and the profile
tab was documented, but neither file ever reached the repository, so awg-ui
could not start at all — `Error: Cannot find module './ma7'`.
Everything that referenced it is gone: the import and the /ui/ma7/profile
route, the three MA7_* variables the CLI passed to awg-ui, the installer
prompts and their cli.env block, and the README section. The panel login
prompt goes back to defaulting to admin instead of the MA7 subscriber login.
awg-ui type-checks, builds and starts again.
Nothing verified a push to main: the only workflow triggers on v* tags, so an
incomplete commit — one whose new files were never staged, leaving an import
pointing at a file that does not exist — sat on main for two days without a
signal.
This runs on every branch push and pull request: bash -n on the installer,
tsc across awg-ctrl, awg-ui and cli, and the Vite build. awg-ui's tsconfig
covers server.ts, so the missing-module case is exactly what the awg-ui step
reports. The same workflow is added for both hosts, since releases are now
published from GitHub and Gitea alike.
awg-ctrl's tsconfig had `include: [""]`, which matches no files at all
(TS18003) — the awg-ctrl step would have failed on the config rather than on
real errors, so it now points at index.ts.
Releases only existed on GitHub, while install.sh now defaults to pulling the
archive from the Gitea mirror. This builds the same archive on a v* tag and
publishes it through the Gitea API, since the runner has no gh CLI: create the
release, fall back to looking it up by tag when it already exists, then upload
the tarball as an asset.
Two details are easy to get wrong and are commented in place. The release id is
the first "id" in Gitea's JSON, so the match must be non-greedy — a greedy one
picks up the author or asset id instead. And the id helper needs `|| true`,
because an empty grep under pipefail would abort the step on the path where the
release already exists.
The workflow cannot run yet: the Gitea repo is a pull mirror, which neither
runs Actions nor accepts release creation. It starts working once the repo is
un-mirrored, Actions are enabled and a runner with the ubuntu-latest label is
registered; the header of the file lists those prerequisites.
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.
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.
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.