504 Commits
Author SHA1 Message Date
leonidorlov-hash 2688a52f08 chore: conn monitor interval 60s -> 600s 2026-08-30 22:14:44 +03:00
leonidorlov-hash 7792234c7c refactor(awg): server-side connection-flood collection, UI unchanged
Before: every 6s UI refresh pulled up to 8MB of raw /proc/net/nf_conntrack
over SSH and detection only ran while someone watched the page.

Now: a background monitor thread (60s) runs one cheap SSH roundtrip per
AWG instance - an awk aggregation inside the container that transfers only
the compact per-IP summary. Snapshot (conn_counts.json) and warnings
(conn_warnings.json) live next to clientsTable; get_clients just reads
those small files (one-off live fallback until the first collector round).
Detection now works 24/7; UI, badges and the trash button are unchanged.
2026-08-30 22:14:44 +03:00
leonidorlov-hash b5efde97f2 chore(awg): restore conn warn threshold to 600 after live testing 2026-08-30 22:14:01 +03:00
leonidorlov-hash b3b1140b59 fix(awg): replace awk conntrack counter with tr/grep/cut (shell-safe) 2026-08-30 20:32:17 +03:00
leonidorlov-hash 1413f3a3ab chore(awg): verbose debug in conntrack counter 2026-08-30 20:27:01 +03:00
leonidorlov-hash e8e8833c96 chore(awg): lower conn warn threshold to 200 for testing + collector debug log 2026-08-30 19:46:26 +03:00
leonidorlov-hash b4b150b4f3 fix(awg): escape $i in conntrack awk (remote shell ate the awk fields) 2026-08-30 19:12:13 +03:00
leonidorlov-hash b4c2e0716b fix(ssh): make connect() idempotent (endpoints re-connect after get_ssh) 2026-08-30 19:00:56 +03:00
hellandClaude Opus 5 92bcf7bb9e Name and package Xray connections the way AmneziaVPN reads them
Three separate problems, none of them new to this branch.

The vless link carried the connection's own label in its fragment, and
vless::Deserialize hands that fragment to extractXrayConfig as the server
description -- so an imported key showed up as "Connection". It now carries
the same node-plus-container name the AmneziaWG configs got.

The `vpn://` key was base64 of the vless link, which the client cannot read:
extractConfigFromData only routes a *bare* `vless://` string to the Xray
branch, and once it is wrapped nothing recognises the payload. That is where
the ErrorCode 900 (ImportInvalidConfigError) came from, and it predates this
branch -- main wraps every protocol the same way.

The QR had the same shape of problem from the other side:
extractConfigFromQr only reaches extractConfigFromData when checkConfigFormat
already recognises the text, and a bare vless link matches none of its
patterns, so a link in a QR code is unreachable however well it scans.

So Xray now goes through the same Amnezia config format as AmneziaWG:
build_xray_client_config ports serialization::vless::Deserialize, wraps its
output the way extractXrayConfig would, and the result feeds both the `vpn://`
key and the QR frame series. Four frames for a typical reality link.

The plain-format QR is still there for v2rayNG and friends, so its label is no
longer "WireGuard".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 14:46:45 +07:00
leonidorlov-hash c215b5fdab fix(ssh): retry once on connect timeout (tolerates random SYN loss) 2026-08-30 07:44:14 +03:00
leonidorlov-hash a662a029ab fix(ssh): pooled connections ignore legacy per-request disconnect() 2026-08-30 07:44:14 +03:00
leonidorlov-hash 2ff82196ef fix(ssh): ensure_connected in SFTP methods too 2026-08-30 07:44:14 +03:00
leonidorlov-hash 772ce96cd4 fix(ssh): devops hardening of the connection pool
- per-manager lock around connect/disconnect: UI handler and background
  monitor can no longer race a half-built transport
- transport keepalive 30s: NAT/firewalls won't silently drop idle tunnels
- one reconnect+retry when exec fails on a dead-but-'active' transport
  instead of surfacing a spurious error to the UI
2026-08-30 07:44:14 +03:00
leonidorlov-hash 48c034fa04 fix(ssh): drop pooled connection when a server is edited or deleted
The pool is keyed by (host, port, username), so changed credentials would
otherwise keep using the stale connection until panel restart.
2026-08-30 07:44:14 +03:00
leonidorlov-hash eb87c2153c perf(ssh): reuse long-lived SSH connections + cache config reads
- SSHManager.ensure_connected(): keep the transport alive, reconnect only
  when it died; run_command auto-heals the connection
- app.py: pool SSHManager per (host, port, username) instead of a fresh
  TCP+SSH handshake for every API call - the main cause of UI timeouts on
  high-latency servers (each 6s refresh paid a 1-2s handshake)
- awg_manager: 10s TTL cache for config path resolution and server config
  reads (was: the same awg0.conf cat'ed 3-4x per refresh); caches are
  invalidated on save_server_config
2026-08-30 07:44:14 +03:00
leonidorlov-hash 4cb5187116 fix(ssh): retry once on connect timeout (tolerates random SYN loss) 2026-08-30 07:40:53 +03:00
leonidorlov-hash 9c5f3fc99d fix(ssh): pooled connections ignore legacy per-request disconnect() 2026-08-30 07:11:07 +03:00
leonidorlov-hash cd5c6c44a1 fix(ssh): ensure_connected in SFTP methods too 2026-08-30 06:48:19 +03:00
leonidorlov-hash 37ce6d5cca fix(ssh): devops hardening of the connection pool
- per-manager lock around connect/disconnect: UI handler and background
  monitor can no longer race a half-built transport
- transport keepalive 30s: NAT/firewalls won't silently drop idle tunnels
- one reconnect+retry when exec fails on a dead-but-'active' transport
  instead of surfacing a spurious error to the UI
2026-08-30 06:41:32 +03:00
leonidorlov-hash 167d4be22b fix(ssh): drop pooled connection when a server is edited or deleted
The pool is keyed by (host, port, username), so changed credentials would
otherwise keep using the stale connection until panel restart.
2026-08-30 06:36:15 +03:00
leonidorlov-hash 6d4d067af7 perf(ssh): reuse long-lived SSH connections + cache config reads
- SSHManager.ensure_connected(): keep the transport alive, reconnect only
  when it died; run_command auto-heals the connection
- app.py: pool SSHManager per (host, port, username) instead of a fresh
  TCP+SSH handshake for every API call - the main cause of UI timeouts on
  high-latency servers (each 6s refresh paid a 1-2s handshake)
- awg_manager: 10s TTL cache for config path resolution and server config
  reads (was: the same awg0.conf cat'ed 3-4x per refresh); caches are
  invalidated on save_server_config
2026-08-30 06:32:27 +03:00
leonidorlov-hash 1f994e6ec4 chore: conn monitor interval 60s -> 600s 2026-08-30 06:07:34 +03:00
leonidorlov-hash 63839c01e2 refactor(awg): server-side connection-flood collection, UI unchanged
Before: every 6s UI refresh pulled up to 8MB of raw /proc/net/nf_conntrack
over SSH and detection only ran while someone watched the page.

Now: a background monitor thread (60s) runs one cheap SSH roundtrip per
AWG instance - an awk aggregation inside the container that transfers only
the compact per-IP summary. Snapshot (conn_counts.json) and warnings
(conn_warnings.json) live next to clientsTable; get_clients just reads
those small files (one-off live fallback until the first collector round).
Detection now works 24/7; UI, badges and the trash button are unchanged.
2026-08-30 05:59:33 +03:00
leonidorlov-hash e43b610391 fix(awg3): use compatibility H1-H4 = 1,2,3,4 per official docs
The updated AmneziaWG docs prescribe 1,2,3,4 when Header Protection is
enabled (custom-header mechanism off, HP hides the message type) - this
is what the native AmneziaVPN client ships. Ranged H with
RandomTrailers=on cause silent transport loss (amneziawg-go#186,
amneziawg-linux-kernel-module#226).
2026-08-30 03:25:51 +03:00
leonidorlov-hash 9e9459787c fix(awg3): use compatibility H1-H4 = 1,2,3,4 per official docs
The updated AmneziaWG docs state that with Header Protection enabled
H1-H4 should stay at 1,2,3,4: the custom-header mechanism is off and
Header Protection (ChaCha20 with the random per-server key) hides the
message type. This is exactly what the native AmneziaVPN client ships.
Ranged H with RandomTrailers=on cause transport packet loss
(amneziawg-go#186, amneziawg-linux-kernel-module#226).
2026-08-30 03:24:34 +03:00
hellandClaude Opus 5 69e3107499 Stamp the login page stylesheet too
login.html carries its own <head> instead of extending base.html, so it
kept requesting the unversioned stylesheet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 03:13:01 +07:00
hellandClaude Opus 5 8536dedc34 Address the review, and stop the browser serving stale CSS
The card header and the QR quiet zone were already correct in the stylesheet,
but nobody was seeing them: /static is served with an ETag and no
Cache-Control, and it sits behind a proxying CDN, so browsers kept the copy
they had and rendered the new markup against the old rules. Stamp the static
links with the newest mtime under static/.

From the review:

- specialJunkField() built ids as `${prefix}Awg${KEY}`, which matches the
  install form's installAwgI1 but not the settings modal's awgSetI1. The
  modal's I1-I5 were therefore never filled in, and saving threw on a null
  element. Callers now pass the whole id prefix.

- join_dns() collapses two blank fields to None, and update_awg_settings()
  reads None as "leave DNS alone", so DNS could never be cleared from the UI.
  The route now tells "not sent" from "sent empty", and an empty value drops
  the line so the built-in default applies again. Same for MTU.

- save_client_config() indexed only DNS into userData, leaving the
  userData.mtu branch of _get_mtu() unreachable. It now indexes MTU too, so
  editing MTU in a connection's config sticks across regeneration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 03:11:35 +07:00
hellandClaude Opus 5 8ae4fcd1e7 Give the QR a quiet zone, and rework the protocol card header
The QR never scanned, before any of this branch's changes: qrcode.js paints
the modules edge to edge with no quiet zone, and against the dark page the
finder patterns sit directly on the background. Simulating a 640x480 capture,
nothing decodes that way -- not the frame series, not the raw config, at any
size. With a 32px white frame around the code all eight frames decode at every
scale tried. So the earlier density work was necessary but not sufficient.

Rounding is dropped from the code itself as well, since a border-radius large
enough to see clips the corner finder patterns.

The card header now carries the protocol name next to its icon, and the
control buttons get a row of their own underneath, where three of them fit
without crowding anything.

Button labels rode high whenever the label opened with a symbol glyph: U+23F9
in the stop button falls back to a font with much taller metrics, which grows
the line box downwards. A fixed line-height keeps the label centred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 03:01:41 +07:00
hellandClaude Opus 5 af87995c87 Fix the protocol card header and make the QR scannable
Two problems with the previous commit's UI.

The header row of a protocol card is a bare space-between flex with no gap,
so the third control button ran straight into the protocol icon on the AWG
cards -- the only ones that get an AWG button. Give the row a gap and let the
button group wrap instead of pushing into the icon.

And the QR: a whole config in one code lands at version 19 raw or 26 wrapped,
while the Android scanner builds ImageAnalysis.Builder() with no
ResolutionSelector and so reads 640x480 frames -- two to three pixels per
module. Emit the frame series ImportController::parseQrCodeChunk reassembles
instead: qint16 magic 1984, quint8 total, quint8 index and the slice as a
QDataStream QByteArray, each frame base64url'd into its own code. At 144 bytes
a frame every code stays under 55x55 modules and decoded at every scale tried
against a simulated VGA capture, including ones where the single code failed.

The WireGuard format stays a single static code, since a raw config is what
AmneziaWG and wg-quick read and it cannot be chunked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 02:37:45 +07:00
Dmitry d0f430bf3f Merge branch 'main' into feat/awg-special-junk-mtu-dns 2026-08-30 02:12:51 +07:00
Dmitry 60c0fe1abf Merge branch 'PRVTPRO:main' into main 2026-08-30 02:11:58 +07:00
hellandClaude Opus 5 1fbcba5c48 Name the server in the vpn:// key and QR code
A plain WireGuard config cannot carry a name: extractWireGuardConfig() in
the client overwrites description with nextAvailableServerName(), so every
imported key lands as "Server 1". Wrap the config into Amnezia's own server
config format instead, which keeps whatever description it is given, and
name it after the node and the container it came from -- "prx-40 AWG3".

The encoding matches ExportController::generateVpnUrl: base64url of
qCompress'd JSON, the size prefix included. The QR payload carries no
vpn:// prefix, because extractConfigFromQr() feeds the scanned text
straight to QByteArray::fromBase64, which drops ':' and '/' but keeps
'v', 'p' and 'n' and would shift the whole payload by three characters.

AmneziaWG and wg-quick only understand the raw config, so the QR panel
gets a format switch rather than replacing what it shows. The named
payload is about half again as long -- QR version 23 instead of 18 --
so it is drawn a little larger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 02:09:31 +07:00
leonidorlov-hash 3847a5667d feat(install): used-port awareness when installing protocols
Previously the install modal defaulted to a fixed port and only avoided
ports of same-family instances, so installing e.g. AWG3 on a host where
AWG2 already held 55424 failed mid-install with a half-created broken
container ('port is already allocated').

- GET /api/servers/{id}/used-ports: real listening UDP/TCP ports via ss
- api_install_protocol: reject a busy port upfront with a clear error
  naming the owning process, before any docker run
- Install modal: fetches used ports, auto-suggests the first free port,
  live-validates manual input (red field + hint + disabled Install button)
- new translation key port_in_use_by (en/ru/fr/fa/zh)
2026-08-29 04:41:06 +03:00
leonidorlov-hash 825b1a6851 fix(ui): don't close modal when drag-select ends on the backdrop
A click starting inside an input and ending outside the modal fired the
backdrop click handler (click target = common ancestor). Now the modal
closes only when mousedown and click both land on the backdrop itself.
2026-08-29 04:36:12 +03:00
leonidorlov-hash b4cd396145 fix(ui): prevent protocol cards flash while server status is loading
The grid rendered all static protocol tiles until the status response
arrived and applyInstalledAppsVisibility() hid the non-installed ones.
Add .apps-pending on the grid (set in HTML, toggled by
setInstalledAppsLoading) that hides tiles while the check runs.
2026-08-29 04:36:12 +03:00
leonidorlov-hash dada400706 fix(ui): enlarge infinity symbol in speed limit badge 2026-08-29 04:35:51 +03:00
leonidorlov-hash fb49d5a450 fix(ui): enlarge infinity symbol in speed limit badge 2026-08-29 04:33:46 +03:00
leonidorlov-hash 85a87454c8 fix(ui): don't close modal when drag-select ends on the backdrop
A click starting inside an input and ending outside the modal fired the
backdrop click handler (click target = common ancestor). Now the modal
closes only when mousedown and click both land on the backdrop itself.
2026-08-29 04:26:19 +03:00
leonidorlov-hash 69a9555083 fix(ui): prevent protocol cards flash while server status is loading
The grid rendered all static protocol tiles until the status response
arrived and applyInstalledAppsVisibility() hid the non-installed ones.
Add .apps-pending on the grid (set in HTML, toggled by
setInstalledAppsLoading) that hides tiles while the check runs.
2026-08-29 04:21:00 +03:00
leonidorlov-hash 47283a41a4 fix: also enable ip6tables+experimental for Docker IPv6 NAT66
Without ip6tables Docker creates no MASQUERADE rule for the
fixed-cidr-v6 ULA subnet, so container v6 traffic leaves with an
unroutable address. Keys are now enforced unconditionally (docker
restarted only on change) so hosts that already have ipv6:true still
pick up the missing ip6tables/experimental keys.
2026-08-29 03:47:38 +03:00
leonidorlov-hash b6f025de33 fix(awg): also enable ip6tables+experimental for Docker IPv6 NAT66
Without ip6tables Docker creates no MASQUERADE rule for the
fixed-cidr-v6 ULA subnet, so container v6 traffic leaves with an
unroutable address. daemon.json keys are now enforced unconditionally
(restart only on change), so hosts already having ipv6:true still get
the missing ip6tables/experimental keys.
2026-08-29 03:47:02 +03:00
leonidorlov-hash 410edd2bce fix: enable IPv6 sysctls at container creation for dual-stack
Even with an IPv6-enabled daemon, containers are created with
net.ipv6.conf.*.disable_ipv6=1, and the flag is fixed at netns creation.
awg-quick then fails on 'ip -6 address add' (RTNETLINK: Permission
denied) and rolls back by deleting awg0 entirely, killing IPv4 too.
Detect host IPv6 before docker run and pass the sysctls at creation.
2026-08-29 03:37:51 +03:00
leonidorlov-hash 9ca0d78d09 fix(awg): enable IPv6 sysctls at container creation for dual-stack
Containers created without net.ipv6.conf.*.disable_ipv6=0 keep the flag
forever (it is fixed at netns creation). awg-quick then fails on
'ip -6 address add' with RTNETLINK Permission denied and rolls back by
deleting the whole awg0 interface - killing IPv4 too. Detect host IPv6
before docker run and pass the sysctls at creation time.
2026-08-29 03:34:04 +03:00
leonidorlov-hash 58e976121f feat(awg): trash button to clear a peer's connection-flood warnings
The torrent-detection warning block under a peer now has a 🗑 button that
wipes the recorded warning history for that peer (conn_warnings.json is
keyed by peer IP; backend resolves the IP from clientsTable).
Adds POST /api/servers/{id}/connections/clear_warnings and
clear_warnings / warnings_cleared translation keys.
2026-08-29 01:12:31 +03:00
leonidorlov-hash c59ccd36e2 feat: per-peer speed limit (Mbps, step 0.1, 0=unlimited) for AmneziaWG
- awg_manager: set_speed_limit() applies tc HTB (egress) + ingress policer
  inside container for peer IPv4/IPv6; limits persisted to bwlimits file and
  re-applied on container start
- maxSpeed stored in clientsTable userData
- UI: speed badge on peer card, speed field with label in rename dialog
- translations: en/ru/fr/fa/zh
2026-08-29 01:10:41 +03:00
leonidorlov-hash f70fd89cb3 feat(ui): field labels above name/speed inputs in peer rename editor 2026-08-29 00:42:51 +03:00
leonidorlov-hash e2d34d69a4 feat(awg): trash button to clear a peer's connection-flood warnings
The torrent-detection warning block under a peer now has a 🗑 button that
wipes the recorded warning history for that peer (conn_warnings.json is
keyed by peer IP; backend resolves the IP from clientsTable).
Adds POST /api/servers/{id}/connections/clear_warnings and
clear_warnings / warnings_cleared translation keys.
2026-08-28 23:33:54 +03:00
leonidorlov-hash b2b4d76cfa feat(awg): per-peer bandwidth limit in Mbit/s (tc in container)
- Rename dialog gains a numeric input (step 0.1, 0 = unlimited) for AWG
  peers; peer list shows a badge: infinity or '⚡X Mbps'.
- Limit stored in clientsTable userData.maxSpeed; enforced via tc inside
  the container: HTB class per peer IP on awg0 egress (download) and an
  ingress policer (upload), IPv4+IPv6.
- Limits are flattened to /opt/amnezia/awg/bwlimits (next to
  clientsTable) and re-applied at container start from start.sh; applied
  live on every clientsTable save.
- API: RenameConnectionRequest.max_speed (-1 = don't touch); works for
  managers exposing set_speed_limit (AWG), ignored elsewhere.
2026-08-28 23:02:56 +03:00
Алексей ЦыганковandClaude Opus 5 0c1ba5ed82 Set dir="auto" on the trigger, search box and options
Usernames and emails are latin even when the panel runs in Persian (the
only locale that sets dir="rtl"). Inheriting the RTL direction pushes such
a string to the right edge and reorders its trailing punctuation, so
"a.zharkova@example.com (user)" renders with the parens and dots in the
wrong place. dir="auto" lets the browser pick per string: latin entries
lay out LTR, Persian ones stay RTL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:40:35 +07:00
Алексей ЦыганковandClaude Opus 5 133dc2bff6 Add substring search to the "assign to user" picker
On a panel with a few dozen users, picking one when creating a connection
means scrolling a plain <select> and reading every entry — there is no way
to type a name.

Adds a small dependency-free searchable select and applies it to the user
picker in the Add Connection modal. It's progressive enhancement: the
native <select> stays in the DOM as the source of truth, so `.value`,
programmatic assignment and `change` listeners keep working exactly as
before (server.html reads `connectionUserId.value` unchanged).

- static/js/searchable-select.js: opt-in via `data-searchable`, with
  case-insensitive substring filtering, keyboard navigation
  (arrows/Enter/Escape/Tab), an empty state, and ARIA combobox roles
- styles built entirely from existing CSS variables, so light/dark themes
  and RTL follow the rest of the panel
- labels come from `data-search-*` attributes, so strings stay in the
  templates; new keys added to all five locales

No jQuery/select2: the panel ships no jQuery today, and pulling in two
libraries for one dropdown seemed a poor trade against ~300 lines of
vanilla JS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:20:15 +07:00