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.
- 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