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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>