Commit Graph
22 Commits
Author SHA1 Message Date
maeneko 67204ddc25 Merge origin/main into the 0.5.0 redesign
ci / check (push) Failing after 6m27s
Brings in 0.4.8: connection log, uninstall and update from the awg-ctrl menu,
secret panel path, stealth hub and sen:// port, random I1/MTU on install.
install.sh keeps the new output (spinner, cards, outdated-install wipe) on top
of the reorganised installer; an outdated install can be reinstalled with a
wipe or removed. The connection log tab uses the MD3 segmented button; its
metrics-token scrape snippet moved to Settings.
2026-10-29 23:07:03 +03:00
maeneko 5b29993d5d Panel redesign, per-server I1 masking, node minimum version 0.5.0, installer output
ci / check (push) Successful in 23m54s
- UI: settings tab with sections (server, API keys, Prometheus, audit, migrate,
  about); server settings moved there; master keys table rework (menu, chips,
  version, compact phone cards); three-state node version chip
- I1: random per-server template (DNS / QUIC Initial / STUN) kept in users.db,
  GET/POST /api/masking, change from the master keys banner
- Hub rejects nodes older than 0.5.0 (close 4026), /ui/nodes reports outdated
- install.sh: spinner for long commands (full output in the log), step progress
  bar, cards; installs older than 0.5.0 require a full reinstall with data reset
- Bump to Beta 0.5.0
2026-10-29 22:59:09 +03:00
Claude 0a9eaa8c8e awg-ctrl: commands for the secret path, connection log, metrics, hub, check
The new features were only reachable from the web panel. awg-ctrl now has:

- panel [path new|off|<path>] — the panel address; a new random, own or no
  secret path (cli.env, panel restarted, VPN untouched);
- log [<search>] [-n N] and log seen — the connection log of this server and
  who was online when, with sen:// device and master key names from ui.db when
  the panel is on the same machine;
- metrics [token|revoke] — the metrics-only token for an external Prometheus,
  printed once with a ready prometheus.yml fragment that carries the path;
- hub [legacy on|off|auto] — nodes with and without the pre-upgrade signature
  and the HUB_LEGACY mode;
- check — what this server gives away: shared pre-randomization VPN values or
  no own I1, no secret path, plain-HTTP panel and subscription, the generic
  "sen"/"awg-node" certificate names, an open hub — each with the fix.

The interactive menu gets items 10–13: panel address and path, connection log,
who was online, and the check. Columns are padded by runes, not bytes, so
Cyrillic names line up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 12:55:39 +00:00
Claude ea07914249 awg-ctrl: update, reinstall and uninstall from the menu
The awg-ctrl menu only started, stopped and changed the panel login; updating
or removing an installation meant finding and running install.sh by hand. The
menu gets "Обновить", "Переустановить" and "Удалить" (7–9), and the same as
`awg-ctrl update | reinstall | uninstall`.

The work stays in install.sh, which now takes ACTION=update|reinstall|uninstall
to skip its own menu. The CLI fetches it — the latest from INSTALLER_URL
(GitHub by default) for update and reinstall, the copy shipped in the release
archive for uninstall, so removal works offline — writes it to a temp file and
execs bash on it in the same terminal. It must not run from /opt/awg-control or
via `bash -c`: kill_related kills every process whose command line holds the
install path, and would kill the installer itself. The CLI passes its original
environment, not the one with cli.env loaded, or the installer would take
UI_PORT, SERVER_NAME and the rest as explicit overrides. The installer deletes
its temp copy on start.

The release archive now includes install.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 12:50:00 +00:00
Claude 1a54d05cca VPN connection log: who connected and when
WireGuard has no connect events, only the last handshake and byte counters.
awg-ctrl now checks them every 30 s and writes conn_log in users.db: connect
at the first handshake, disconnect at the last activity (the last tick the
counters moved), with the session length and traffic, plus peer_seen, the
last time each peer was online. Open sessions are restored from the database,
so a restart adds no duplicates; a down interface is not a disconnect; a peer
removed mid-session is closed. Events keep the name, owner and VPN IP as of
that moment and live 90 days. Client public IPs are not recorded.

GET /api/connlog serves the events and the per-peer last-seen list. The panel
fetches it from the selected server at /ui/connlog and fills in master key
labels and device names for sen:// peers. A new "Подключения" tab shows the
event feed with search and paging, and "who was online when", including
those who never connected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 12:06:40 +00:00
Claude fd5e2cc761 Hub: node signature before the WebSocket upgrade
The hub accepted a WebSocket from anyone on /node/v1, a path in the open
code, and replied with a hello nonce: one connection was enough to tell a
scanner what it had found. The agent now signs the upgrade request itself
(X-Node-Id, X-Node-Ts, X-Node-Sig; on first binding also the join secret
and its key), and the hub checks it before Accept. Without a valid
signature it answers Go's plain "404 page not found". Inside the socket
the protocol is unchanged, and the auth frame must come from the node that
signed the request.

Older agents send no signature. The hub lets them in while some node has
never connected the new way (nodes.preauth), logs them as outdated, and
closes that door by itself once every node is updated; HUB_LEGACY=on/off
overrides. The timestamp window is a day: node clocks drift, and the
headers travel inside pinned TLS. The agent treats a 404 at upgrade as a
rejection and backs off for a minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 11:58:06 +00:00
Claude a3d4664214 Stealth sen:// port, random cert names, external Prometheus card
The subscription port answered every request with signed JSON: one probe
told a scanner what it had found, and the public key from any sen:// link
pinned the exact server. It now speaks the protocol only to those who prove
a secret — peek and register with the link's master secret, everything else
with a valid device signature — and gives Go's plain "404 page not found"
to the rest. A deleted device still gets its 410, but only when the request
is signed with its key, kept in the new revoked_devices table. The migration
freeze answers only proven clients too.

Self-signed certs for the subscription (SUB_TLS=on) and the node hub got
fixed names (sen, awg-node); new ones get a random host-like name. Clients
and nodes pin the key, so existing certs keep working.

The panel could already issue a metrics-only token, but nothing in the UI
used it. The Monitor tab gets an "External Prometheus" card: issue, reissue
and revoke the token, with a ready prometheus.yml fragment whose address is
taken from the page, so it carries the secret path or the nginx subfolder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 11:52:55 +00:00
Claude 13da9a7200 Secret URL path for the panel
The panel port is open to the internet, and its login page and JS bundle
gave it away to any scanner in one GET. The installer now generates a
random UI_PREFIX: from outside the panel answers only under
http://IP:PORT/<prefix>/ and gives Go's plain "404 page not found" to
everything else. Loopback needs no prefix (CLI, local Prometheus, nginx on
the same host); while a move code is out, POST /migrate/v1/import is let
through without it, since the code carries no path.

A reinstall with kept data keeps the prefix; UI_PREFIX=off or a custom value
can be set before running the installer. A moved panel brings its prefix in
UI_PREFIX_MOVED, so old bookmarks and API clients keep working through the
relay. awg-ctrl status and credentials show the full panel address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 11:41:09 +00:00
Claude aa848de31c Hide the brand from unauthenticated requests
GET /ui/brand answered without auth with {"brand":"Forgetting",…}: one
request was enough for a scanner to tell a found port is this panel. It now
requires the panel JWT (Go and the Node reference alike). The login screen
says just «Вход», the page title is neutral until login, and the wordmark
loads after sign-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 11:30:40 +00:00
Claude 63e84e0df7 Random I1 and MTU on install
I1 becomes a DNS query to a random popular domain (A/AAAA) instead of the
shared icloud.com response every key carried. The format sticks to tags both
the kernel module and amneziawg-go accept and is checked before awg1.conf is
written: a bad I1 fails the whole awg-quick up. With KEEP_DATA=y the old I1
is kept, or generated if missing — the server never checks it, old keys keep
their own.

MTU is picked from 1320–1376 (AWG_MTU overrides) and kept on reinstall.
awg-ctrl now reads MTU from awg1.conf for vpn:// keys instead of a constant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013bNGYiDst62wH3ZGTRPFLJ
2026-10-28 11:26:07 +00:00
maeneko 96c21408e6 Prometheus on a node; panel under a URL prefix; open ports on migrate import
A panel without VPN (e.g. after the move) can use a Prometheus that lives on
a node, such as the one with history left on the old server (node #0). Both
directions go through the agent's WSS: the node's Prometheus scrapes
127.0.0.1:AGENT_METRICS_PORT/metrics, the agent checks its token and asks
core (metrics → metrics_res, the same exposition as /metrics, at most once
per 5 s per connection); the monitor tab reads it back through the agent,
which forwards only GET /-/ready and /api/v1/query[_range]. The panel finds
it by itself (local first, then the lowest-id node), and the install/remove
buttons work on a panel without VPN. `monitor install` on a node without its
own panel picks AGENT_METRICS_PORT, writes it to cli.env and restarts the
agent; on the old server it keeps the collected data.

The panel works behind nginx in a subfolder: relative asset and API paths,
relative directory redirects, and the client address from X-Forwarded-For /
X-Real-IP only from trusted proxies (loopback, TRUST_PROXY).

migrate import opens the moved SUB_PORT/NODE_PORT in UFW when it is active.
2026-10-21 19:25:54 +03:00
maeneko eb19cacf93 Move the panel to a server without VPN; servers for all master keys at once
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.
2026-10-21 15:59:33 +03:00
maeneko 3511b8c6c3 Master keys: a down server no longer breaks the device list, 403 for keys outside the API key's scope
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.
2026-10-20 20:39:09 +03:00
maeneko d6fc2049c3 Master keys tab: same columns as users, device IPs, per-key summary
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.
2026-10-19 12:23:21 +03:00
maeneko de8a32ac41 Fix awg-ctrl hanging on the torrent flag; log out on an expired session
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.
2026-10-18 00:08:17 +03:00
maeneko c73da602d6 Block BitTorrent on the server
- 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
2026-10-17 23:35:30 +03:00
maeneko 105f184a93 Monitor: 7 and 30 day views, traffic used, merged tiles
- 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
2026-10-17 21:36:51 +03:00
maeneko 5aa0c3a2f5 Port the CLI to Go: awg-control <start|stop|…>
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.
2026-10-14 13:27:24 +00:00
maeneko 70bb587216 Port awg-ui to Go: awg-control ui
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.
2026-10-14 13:27:24 +00:00
maeneko c2e9636f31 awg-ctrl: keep CPU mode order in /api/metrics; diffbench compare and host runner
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
2026-10-14 13:27:24 +00:00
maeneko 016d3de62d Port awg-agent to Go: awg-control agent
- 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
2026-10-14 13:27:24 +00:00
maeneko 59e076c1de Start the Go rewrite: jsjson, awg-ctrl port, differential bench
- 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
2026-10-14 13:27:24 +00:00