The «Перенос» tab moves a panel + VPN server to a panel-only server without
users noticing: the new panel issues a one-time awgmove:// code, the old one
sends its ui.db, subscription keys and hub certificate encrypted with it, and
becomes node #0 of the new panel. Server id 0 is kept, so config servers[] is
byte-identical. The old server relays its former ports (awg-relay, PROXY v1
header trusted only from RELAY_FROM), so sen:// links, nodes and the API keep
working. The same is available from the CLI: migrate export/import/cancel,
rehome.
Nodes now learn the panel's address: the hub sends it in ready, the agent
stores it in CORE_ENDPOINT, dials it first and confirms with core_ok. The
new panel shows when the old server can be turned off: devices that got the
new address, nodes that confirmed it, master keys never opened.
«Серверы для всех» on the master keys tab adds or removes servers on every
live key in one go, with a dry run that warns about API keys that would lose
sight of some keys, and sets the servers new keys get.
masterDevices skips a server that did not answer and reports it as
unreachable; peers are fetched once per server per pass. The tab loads each
key on its own, counts devices from the key when the list is missing (and still
asks before unbinding), drops stale responses, labels an IP from another
server and blocks a double create. masterFor answers 403 instead of 404 for a
key that exists but is outside the API key's servers, so a client does not
mistake it for a deleted one. updateMaster checks every statement in its
transaction.
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.
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
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