Commit Graph

496 Commits

Author SHA1 Message Date
MHSanaei 0972ac31eb fix(clients): collapse repeated inbound ids before fanning a client out
Create, BulkCreate, Attach and BulkAttach built one marshalled copy of
the client per entry of the request's inboundIds and held all of them
until the fan-out returned, with repeated ids never collapsed. An id
costs two bytes of request body, so a large client named on the same
inbound a few thousand times needed gigabytes and the panel died.

Every fan-out entry point now collapses the id list first, keeping the
order of first occurrence so credentials are still minted on the first
inbound.
2026-11-01 08:44:16 +01:00
MHSanaei f8ceede46f fix(hwid): keep a subscription's device limit while its client is disabled
effectiveHwidLimitForSubID took the limit over enabled clients only. A
client switched off for expiry, quota or by hand therefore had no limit
at all, while the subscription body is still served for disabled
clients, so any number of devices registered and stayed past the
renewal.

The limit now follows the subId whatever the enable flag, and the gate,
the slot status and the trim keep sharing that one query.
2026-11-01 08:39:50 +01:00
MHSanaei bd36053356 fix(clients): match non-ASCII capitals in the shared-email checks
The checks that decide whether a client_traffics row is still used by
another inbound, whether an email is already taken under another subId,
and which depleted rows a scoped delete may drop compared a Go-lowered
email with LOWER(clients.email), which SQLite folds for ASCII only. An
email such as Иван was never seen as attached elsewhere, so the node
sweep deleted the usage row of a client the master's own inbound still
served and the client outlived its expiry and quota.

Each check now matches the lowered or the stored spelling and keys the
result in Go, so ASCII behaviour is unchanged.
2026-11-01 08:37:01 +01:00
MHSanaei 7f64a67ba5 fix(clients): re-enable only quota-disabled clients on a traffic reset
Every traffic reset re-enabled its clients unconditionally, in the
client record and in the traffic row a master treats as the client's
state. A master forwards an inbound's scheduled reset and "reset all"
to its nodes through the per-client reset calls, so a client disabled
by hand on a node inbound came back on at the next scheduled reset.

Each reset now decides, before it zeroes the counters, which of its
clients the quota switched off, counting cross-panel usage, and
re-enables only those; a client disabled below its quota keeps its
record and traffic row off. The reset API docs say so.
2026-11-01 08:31:27 +01:00
MHSanaei 67820dc20d fix(clients): keep a hand-disabled client off through auto-renew
autoRenewClients re-enabled every renewing client whose traffic row was
off, as if only the panel's quota and expiry check cleared that flag.
The operator's disable routes (the client editor and bulk disable)
clear it too, so a client disabled by hand came back on with a fresh
period and zeroed counters as soon as its Auto Renew period ended.

A renewal now restores only a client the lifecycle job switched off:
its quota is spent, counting cross-panel usage, or its record was
stamped at or after its deadline, which the job does and a disable of a
live client cannot.
2026-11-01 08:23:48 +01:00
MHSanaei 30f17ef5a3 fix(amneziawg): keep port forwards per inbound and off future relay ports
Attach and BulkAttach copied a client record's forwardedPorts onto the
target inbound. A client a node reports keeps the forwards the node
chose, so attaching it to one of the master's AmneziaWG inbounds opened
listeners of the node's choosing on the master, among them the
loopback relay port a later AmneziaWG or TUIC inbound derives from its
id; that inbound's traffic then went to the forward and Xray failed to
bind.

A forward is set per inbound and is no longer carried by an attach, and
creating an AmneziaWG or TUIC inbound is refused while a peer forward
already holds its relay port.
2026-11-01 01:22:51 +01:00
MHSanaei fc8efc0745 fix(node): bound what one node inbound adds to a subscription
A subscription renders an inbound's stream and remark once per client
and per external proxy, and every client holding its subId, all values
a node chooses: an inbound with a megabyte stream and thousands of
external proxies, or many clients sharing one subId, made a single
unauthenticated request cost gigabytes of memory and minutes of CPU.

A reported inbound whose streamSettings exceeds 64 KiB, sniffing 16 KiB
or remark 1 KiB, or that lists more than 64 external proxies, is no
longer taken in (an existing one keeps its stored values), and a node
can no longer give a client a subId another client already holds,
including another one in the same snapshot.
2026-11-01 01:11:00 +01:00
MHSanaei f9f21fd8a4 fix(node): bound the host groups a node's inbounds may adopt
A node-reported host group is stored once per address and per adopted
inbound, so the rows and bytes a merge wrote were the product of three
values the node chooses, unbounded by the response size cap: a small
hosts/list answer could fill the master's disk.

A group whose serialized form exceeds 16 KiB is no longer adopted, and
a node's inbounds hold at most 2000 host rows, counted over what they
already hold, so repeated merges do not add more; the merge logs how
many groups it left out.
2026-11-01 01:05:48 +01:00
MHSanaei 1c748adadf fix(sub): resolve a blank subId to no subscription
A subId is the only selector of a subscription's inbounds and external
links, so a blank one matched every client stored without a subId, as
clients embedded through inbounds/add or inbounds/import are. The
Telegram bot authorised a client by email and then looked its links up
by that blank subId, handing a bound user the working links of every
other subId-less client.

A blank subId now resolves to nothing in the subscription service, and
the bot builds a subId-less client's own links by email, as the panel's
client page does.
2026-11-01 00:57:32 +01:00
MHSanaei 9a95153f0e fix(hwid): track devices only for subscriptions a client owns
The subscription routes run the HWID gate before they look up the
subId, and the gate read "no device limit" and "no such subscription"
alike, so a request naming any path segment with an X-HWID header was
answered 404 yet stored that segment, up to about 1 MiB, in
client_hwids for good. Anyone reaching the subscription port could fill
the disk until the panel could no longer save anything.

An unlimited subscription is tracked only while an enabled client owns
its subId, and a one-time seeder drops the rows already stored for
subIds no client owns.
2026-11-01 00:54:59 +01:00
MHSanaei 2b9adaa6ff fix(clients): set a client's Telegram user on every inbound it is on
SetClientTelegramUserID patched tgId in the settings of only the
inbound the client's traffic row points at. Sibling inbounds kept the
old value, and the next sync of any of them wrote it back into the
client record, so removing a Telegram user from a multi-inbound client
silently re-bound that user to it.

The write now goes through applyClientFieldByEmail, as the other
by-email setters do, so every attached inbound, on the master or a
node, receives the new tgId.
2026-11-01 00:51:31 +01:00
MHSanaei 6a0c983c89 fix(node): keep only the latest refused claims for the log-once state
The set that kept a refused node claim's warning to one line was keyed
by the reported email and never forgot one. Some refused emails, such
as a padded one or a new client holding a foreign subId, match no row
on the master, so a node rotating them grew the set without bound; a
snapshot made only of padded entries also skipped the warning.

Each node now keeps just the emails its latest snapshot was refused,
both return paths log, and the logged email is cut to 64 characters.
2026-11-01 00:47:46 +01:00
MHSanaei bebe1b911d fix(ldap): keep expired and over-quota clients off on directory sync
The LDAP sync enabled every disabled client whose directory flag was
true, without asking why it was off. A client the traffic job had just
disabled for a passed expiry or a spent quota therefore came back on
every sync and connected again for the seconds until the next traffic
pass, for as long as the directory listed it.

The sync's enable step now drops the clients the panel's own depletion
rule keeps off, and enables nobody in a run whose lookup fails. A
client the operator renews is enabled at the next sync.
2026-11-01 00:44:05 +01:00
MHSanaei c92c7718bf fix(xray): refuse xhttp downloadSettings the core's dialer cannot use
ValidateOutboundConfig, the save-time gate shared by template saves,
API saves and outbound subscription refreshes, relied on conf.Build,
which accepts a downloadSettings block with no address or on another
transport. The core's XHTTP dialer dereferences that address and
asserts that transport on every dial, so an imported link or a
subscription carrying such a block panicked Xray, and every user's
connections with it, whenever traffic was routed to that outbound.

The built sender settings are now checked for both, and the outbound
is refused with a message that names the missing part.
2026-11-01 00:41:37 +01:00
MHSanaei 152c84efa2 fix(inbounds): delete an inbound's dependent rows on every delete path
The node sync's sweep of an inbound the node stopped reporting deleted
the inbound but left its host rows, and the depleted-clients delete
left its sub-balancer references. SQLite hands a freed inbound id out
again once the table empties, so a node's host overrides could bind to
a new inbound it never hosted and point that inbound's subscribers at
an address, port and TLS settings the node chose.

delInbound's cleanup moves into deleteInboundRow (detach clients,
delete the inbound, its host rows and its sub-balancer references),
which the sweep and the depleted-clients delete now call as well.
2026-11-01 00:36:45 +01:00
MHSanaei 776ca554bd fix(server): keep only the requested tail while reading the access log
GetXrayLogs held one entry for every line of Xray's access log and cut
the list to the requested count only at EOF, so the panel's memory grew
with the file. Xray writes a line for every refused request, which puts
the file's size in the hands of anyone who can reach an inbound port,
and the periodic prune does not bound it between runs. A negative count
also panicked on the final slice.

Entries beyond the newest count are dropped while scanning, and count
is clamped to 1..10000 with 100 as the fallback, as GetAmneziaWGLogs
already does.
2026-11-01 00:31:59 +01:00
MHSanaei c3a32c8343 fix(shadowsocks): keep a 2022 inbound with no enabled client locked
When every client of a Shadowsocks-2022 inbound was disabled the panel
handed the core an empty client list, from which xray-core builds a
single-user server keyed by the inbound's server key alone. That key is
the first half of every link the panel ever issued, so a client cut off
for quota, expiry or by the operator connected again with it.

An inbound left without clients now gets a placeholder user whose key
is random and held only in memory, in the config and in the running
core alike. The core stays multi-user, and disabling the last real
client takes the same remove-and-restart path as any other client, so
its live sessions end as well.
2026-11-01 00:30:14 +01:00
MHSanaei 07bbf4788a fix(node): remove a client a pushed inbound disables from the running core
UpdateInbound applies a master's push to the node's running core as the
difference between the stored and the pushed user sets, but both sets
dropped clients whose traffic row is off, and the same transaction had
already stored the pushed flags on those rows. A client the push
disabled therefore vanished from both sets, no RemoveUser was sent and
the node's Xray kept serving it.

The running set is now taken from the stored row's own enable flags,
so the client the push switches off is removed from every handler,
including a second inbound sharing the same traffic row.
2026-11-01 00:24:29 +01:00
MHSanaei daf5b16eda fix(wireguard): hold /0 allowedIPs to the overlap guard
The tunnel overlap guard treated every /0 allowedIPs entry as claiming
no address, while the entry was stored with its host bits and the
consumers read them back: the AmneziaWG port forwarder resolves a peer's
target from them and the device routes the /0 by longest prefix. An
entry such as 10.8.1.2/0 therefore passed next to another client's
10.8.1.2/32, and forwarded ports reached that client's device.

A written entry now counts with its whole range, so a /0 collides with
every held address of its family. On the stored side only a literal
0.0.0.0/0 or ::/0 default route keeps nobody out, and a /0 with host
bits claims that host. Editing a legacy peer that echoes its stored
default route still works.
2026-11-01 00:06:09 +01:00
MHSanaei d114845e44 fix(clients): store client group names trimmed
The groups page lists clients.group_name as stored, while every group
action trims the name it is given. A name with surrounding white space,
saved by a node snapshot or by a client edit, was listed as a separate
group, and deleting, renaming or resetting that row acted on the real
group of the trimmed name instead.

Group names are trimmed wherever a client record is built or updated,
and a one-time seeder folds stored names onto their trimmed form.
2026-11-01 00:03:48 +01:00
MHSanaei 376ab34c0f fix(clients): purge depleted clients by their exact usage key
DelDepleted turned the emails of depleted usage rows into client
identities through BulkDelete, which trims each email for request
bodies. A depleted row keyed by a padded email, which a node's stats
could store and which belongs to no client, therefore resolved to the
healthy client it resembled and deleted it. Rows whose key carries
surrounding white space name no client and are skipped.
2026-11-01 00:02:25 +01:00
MHSanaei 7907843941 fix(node): let a node change only the clients it alone hosts
A node snapshot was merged into the master's client records by email, and
the only ownership check filtered one derived copy of it: the node's
settings were still stored and re-synced later, its stats still merged,
and a record linked to the master as well as the node took every value
the node reported, including credentials rendered into the master's own
Xray config.

Each snapshot now passes one gate before anything is written. A record
also linked to the master or another node keeps the master's values
apart from per-link fields and the lifecycle the merge already settles;
a record linked only elsewhere, a detached record, a padded or
case-variant email and a new client holding another client's subId are
dropped from the stored settings as well. A soft-orphaned record can be
taken back only by the node whose merge orphaned it, which the new
clients.sync_orphaned_by column records. A node tunnel no longer owns
the peer keys of a client a master-local tunnel also serves.
2026-11-01 00:02:12 +01:00
MHSanaei f66e369e41 fix(node): keep pushed inbounds off the node host's own resources
A node-sync token, or a master enrolled with the mTLS certificate, may
push inbounds, and the node stored them as already validated. An
inbound is effectively config for a core running as root, so a push
could serve the host's files (hysteria file masquerade), relay to its
own services or the core API (masquerade proxy, fallback dest, REALITY
target), write files (masterKeyLog), use local or templated XDRIVE
storage, fetch URLs (ECH, realm masks), or run tun/http/mixed inbounds.

A push now refuses those fields unless the node already stores the same
value, which keeps rows configured by the node's own admin syncing:
relay targets must be public or another of this node's inbounds, and
only node-eligible protocols are accepted. Every caller is refused env:
references, which the core expands from the panel's environment, and
the inbound listen address is JSON-encoded into the config instead of
quoted by hand.
2026-10-31 23:30:02 +01:00
MHSanaei 5cf2ab4f69 fix(tuic): bound the timeout settings at a day
max_idle_time and authentication_timeout are second counts multiplied
into a Duration with no bound, so a stored value past the int64 range
wrapped negative and the QUIC listener panicked on the first
ClientHello, taking the panel down at every start until the row was
fixed. Saves now refuse more than a day, and a stored row past it fails
its own listener start instead.
2026-10-31 14:53:20 +01:00
MHSanaei 425a2bbf08 fix(amneziawg): bound what a handshake allocates and the tunnel MTU
Every handshake initiation builds Jc junk packets of Jmin..Jmax bytes
and the I1-I5 signature packets, and the limits allowed the full uint32
range and lengths of any size, so one connection made the embedded
device allocate without bound. The MTU reaches the device as an int32
and a wider value made it panic. Both ended the whole panel at the next
connection, after every restart.

Jc is now at most 128, Jmin/Jmax and each signature packet at most one
65535-byte datagram, and the MTU at most 65535. Saves refuse more, the
device setup refuses rows stored before, and the stored MTU is capped
where it is read. The form schemas carry the same limits.
2026-10-31 14:50:18 +01:00
MHSanaei dcdc56c64b fix(traffic): stop adding capped counters in SQL
Per-client counters are stored up to TrafficMax, which a node can reach
by reporting huge usage, and the group totals, group baselines and the
per-client reset lookup added two such counters inside SQL, overflowing
it: every group listing and reset failed, and no client on that reset
cycle was reset. The counters are now read separately and added in Go,
saturating at TrafficMax.
2026-10-31 14:41:12 +01:00
MHSanaei d69b924baf fix(wireguard): step over wide address claims when allocating
allocateWireguardAddress walked the scope one address at a time and
tested each against every wide claim, so a claim covering an IPv6 /64
(a node can report such allowedIPs) turned the next address-less client
add into a 2^64-step loop that also held every later client change on
that inbound. The scan now jumps past a wide claim in one step, so it is
bounded by the number of claims.
2026-10-31 14:35:01 +01:00
MHSanaei 351a01ff0c fix(clients): bound the auto-renew interval at a century
An interval renewal turned the client's reset days into milliseconds
with no bound, so a value that wraps int64 (a node reports it unchecked)
sent the catch-up loop stepping through time for seconds per client
while the traffic writer held the database. Intervals above 36500 days
no longer renew, and every client write refuses them.
2026-10-31 14:32:31 +01:00
MHSanaei 44a502a407 fix(clients): probe inbound settings with a short literal pattern
Deleting a client by an email no record holds fell back to a settings
LIKE '%email%' over every inbound with the raw route parameter, which
costs SQLite the settings length times the pattern length while it holds
the database; a long near-miss email froze the panel for minutes, and
wildcards were not escaped. The pre-filter now uses the first 64 bytes
of the email, escaped; the exact client match after it still decides.
2026-10-31 14:24:28 +01:00
MHSanaei a69f2dee13 fix(node): keep a node's online and IP reports to its own clients
A node's online tree and both client-IP reports were stored as sent:
they could name any client of the master or another node, under any
panel's GUID, dated into the future, and in any number. That marked
master clients online, fed source addresses for clients the node does
not serve into the IP limit and its fail2ban log, and planted rows the
retention sweep never removed.

Each report is now cut to the clients linked to the node's inbounds and
to the node's own key and its sub-nodes' origins (never this panel's or
another node's GUID); timestamps are capped at now, and one client's
list from a node keeps at most its 256 newest entries.
2026-10-31 14:17:43 +01:00
MHSanaei 4d27313c5e fix(node): give each node one push budget per traffic sync
After a sync tick disabled clients, the master pushed every affected
node inbound with its own 4 s timeout, a few at a time, and the job
served no other node until all were done; a hanging node could stall it
for as long as it had inbounds to report. Pushes are now grouped by node
and each node shares one budget, still a few inbounds at a time; what
does not make it stays for the reconcile, the node being dirty already.
2026-10-31 14:07:08 +01:00
MHSanaei d005d89a8b fix(node): settle each queued node reset on its own
A per-client reset the node never accepted stopped the delivery loop:
resets queued before it were re-sent on every sync, each time wiping the
usage made since, and those after it were never sent. Each reset now
settles by itself, a refused one moves to the back of the queue, and the
loop goes on until the context is spent.

A node also delivered its master's reset to its own sub-nodes inline, so
one silent sub-node held the master's request until it timed out and was
replayed; the inline attempt now waits at most a second and the rest
finishes, or stays queued, in the background.
2026-10-31 14:01:45 +01:00
MHSanaei f67111a08a fix(traffic): keep enforcing limits when one inbound's settings are broken
Disabling depleted clients rewrote each affected inbound's settings and
aborted the whole lifecycle tick when one blob did not parse, so a node
storing a malformed blob on the master switched expiry and quota checks
off for every inbound. Unparseable node settings are now treated like a
snapshot that drops the hub's clients (the hub keeps its copy), and an
unparseable stored blob is left as it is while the client's rows are
still disabled; the rewrite also works one level deep like the others.
2026-10-31 10:43:40 +01:00
MHSanaei a203056949 fix(node): rewrite a node's settings blob without decoding all of it
Dedupe, tombstone stripping and the lifecycle lift decoded a node's
whole settings blob into generic maps and wrote it back indented, so a
deeply nested value anywhere in it cost the square of its depth on every
merge (10 KB became 50 MB). They now split the blob one level deep, read
only email, expiryTime and enable of each client entry, keep everything
else as raw text and write the result compactly.
2026-10-31 10:39:13 +01:00
MHSanaei 8ca321138b fix(traffic): keep lifecycle statements under SQLite's variable limit
Three statement shapes bound one SQL variable per row and failed past
SQLite's limit, which a node can reach by itself with the clients and
inbounds it reports: the depleted-client lookup and disable in
disableInvalidClients (stopping every expiry and quota check), the
whole-slice Saves of renewed inbounds and traffics (stopping renewals),
and Preload("ClientStats") over all inbounds (stopping config builds).
The IN lists are chunked, the Saves go 100 rows at a time, and client
stats are attached in chunks instead of preloaded.
2026-10-31 10:34:35 +01:00
MHSanaei fd3fa86982 fix(node): drop a swept inbound's bookkeeping from the node runtime
The node Remote keeps a fingerprint, a remote id and possibly an alias
per inbound tag. When the master swept an inbound the node stopped
reporting, those entries stayed, so a node rotating tags grew the maps
for as long as the master ran. The sweep now hands every removed tag to
Remote.ForgetInbound, which drops all of it in both tag forms, and a
master-driven delete uses it too. The sweep's call is wiring;
TestRemoteForgetInboundDropsEveryTrace covers what it drops.
2026-10-31 09:28:39 +00:00
MHSanaei d4b80b5d80 fix(node): merge each node inbound once and sweep what the node dropped
Two shapes of a node's inbound list cost the master without bound:
- an inbound listed many times was merged once per listing, every pass
  walking its whole client set again; the first listing now wins and the
  repeats are dropped;
- disabled inbounds were never swept, an exemption meant for inbounds the
  master created but has not delivered yet, so a node could report any
  number of them and the master kept them all; the exemption now covers
  only rows without an origin, i.e. never reported by the node.
2026-10-31 09:26:31 +00:00
MHSanaei 87f3f54ea6 fix(node): bound the panel GUIDs a peer reports
A peer's panelGuid is copied into nodes.guid and into every inbound row
it reports (an indexed column), a forwarded origin GUID likewise, and a
GUID-keyed IP tree or a master's global-traffic push keys one row per
client. One oversized value therefore multiplied into gigabytes stored.
GUIDs over 64 bytes (panels mint 36-character UUIDs) now fail the
heartbeat, fall back to the reporting node's key, or are dropped.
2026-10-31 09:22:24 +00:00
MHSanaei 08482154c2 fix(node): keep master requests on the node they were sent to
The master's node clients followed redirects to any host, presenting the
same panel-wide credentials (the mTLS client certificate) and replaying
the request there, so one node could have the master push to another.
Node clients and the certificate-pin probe now stop at a redirect off
the original host:port and report the 3xx as the node's answer. The
probe change is wiring; TestNodeClientsDoNotFollowARedirectToAnotherHost
covers the guard.
2026-10-31 09:14:57 +00:00
MHSanaei f1e1b0fb0c fix(i18n): publish the translation bundle only once it is complete
InitLocalizer filled the shared bundle in place, and the Telegram bot
runs it again while the panel is serving, so a request could read the
bundle's maps while they were being written and the process died on a
concurrent map access. Build the bundle privately and publish it, and
the bot and web localizers, through atomic pointers; the lazy load in
the middleware publishes only if nothing else did first.
2026-10-31 09:11:54 +00:00
MHSanaei 2e14a2a51a fix(web): refuse oversized login usernames before recording them
A failed login keeps its username in the in-memory log buffer, the
limiter table and the notifier queue, and the route accepted any body
the global cap allowed. The login route now takes at most 16 KiB and
answers a username over 256 bytes with the generic failure before any
of those see it; the user setters refuse such a name too, so no stored
username can be one the login route turns away.
2026-10-31 09:04:27 +00:00
MHSanaei 534afe13ab fix(sub): treat external subscription providers as untrusted input
A client's external subscription is fetched from a third party on every
subscriber refresh, and the fetcher trusted it more than its siblings:
- it dialled any address, redirects included; it now goes through the
  SSRF guard like the outbound subscription and remote routing fetchers;
- it forwarded any line containing "://"; it now keeps only server share
  links, so app deeplinks and plain URLs never reach the subscriber's app;
- it bounded the body but not the link count (now 1000) or the headers
  (now 64 KiB), and stored the raw error, URL included, on every row; the
  stored error is now host-only and at most 1 KiB.

The link parser also refuses links that nest JSON past 16 levels, since
the JSON subscription pretty-prints it at quadratic cost, and external
configs are marshalled compactly until the final, single indent pass.
2026-10-31 08:53:27 +00:00
MHSanaei 61ee202750 fix(xray): bound what the in-process config loader is asked to expand
The panel runs xray-core's conf.Build() in its own process on inbounds
and on outbounds that come from pushes and subscriptions. Some values
make it allocate or loop without bound (an XHTTP sessionIDLength, udphop
port lists, any number of finalmask entries, each xmc one costing an RSA
key), and certificate paths are read whole whatever they name.

Refuse those before any build: sessionIDLength above 1024, more than one
port range's worth of udphop ports per stream, more than 16 masks per
stream, more than 64 xmc masks per outbound subscription, and certificate
or key files that are not regular files of at most 1 MiB. Inbound saves,
the TUIC readers and the certificate-pin helper apply the file rule too;
the pin helper also stops looking at paths that name files on a node.
2026-10-31 08:43:30 +00:00
MHSanaei db20e491e6 fix(net): stop per-request clients parking connections to remote hosts
The node certificate probe builds a transport for one request and dropped
it with the connection still parked, and the remote routing client kept
idle connections with no timeout; either stayed open for as long as the
remote side wanted. Close the probe's idle connections when it returns,
and turn keep-alive off for routing downloads, which are minutes apart.
The routing client dials only public hosts, so no loopback test reaches
it; TestFetchCertFingerprintLeavesNoIdleConnection covers the probe.
2026-10-31 08:32:58 +00:00
MHSanaei f1444bec3f fix(outbound): store subscription refreshes without trusting the row read
The refresh rewrote the whole row it read before a download whose length
the provider controls, so an admin's delete, disable or edit made in the
meantime was undone; Update had the mirror problem. Both now write only
their own columns, and a refresh of a row that was deleted or re-pointed
stores nothing.

Around the same path:
- the preview fetches without creating (and leaking) a scratch row, and
  Create keeps enabled=false instead of falling back to the column default;
- fetch errors name the host but not the subscription URL, which is a
  credential and ends up in the log and in lastError;
- each fetch closes its transport's idle connections;
- one document loads at most 2000 links, and the cut is reported;
- wireguard outbounds from a subscription always run on the userspace TUN;
- editing a subscription schedules an Xray reload like delete and move do.
2026-10-31 08:31:03 +00:00
MHSanaei 1fa8cc785d fix(outbound): keep imported links to remote-server settings
A share link describes a remote server, but extra=, fm= and the vmess JSON
copy provider objects verbatim, so a link could carry keys that only make
sense on the importing host. Both link parsers now drop dial-redirect and
host file keys (matched case-folded, as the core binds them).

Outbound subscriptions also refuse outbounds that dial this host or its
network without allowPrivate, any that target the core's own API or
metrics port, and any that take their server from DNS records. Rows
stored earlier are filtered the same way when the config is built.
2026-10-31 08:19:44 +00:00
Hamed 8eb58a4a14 perf(ip-sync): avoid redundant client IP reads and writes (#6930)
The access-log scanner and the cross-node merge stored the same IP list in
opposite orders, so neither saw a row as unchanged, and the merge re-read
every preloaded row and re-upserted every attribution row each sync cycle.
All writers now share one canonical encoder (timestamp, then IP ascending),
the first CAS is seeded from the preload, and identical attribution rows are
skipped. A create race between the scanner and node sync now merges into the
winning row instead of dropping the loser's IPs, which on PostgreSQL also
aborted the scanner's whole scan transaction.

Closes #6927
2026-10-25 14:13:18 +01:00
Hamed 952c1c751a perf(inbounds): batch client identities for stats enrichment (#6929)
backfillClientStats loaded every column of every client once per inbound,
although enrichment only reads UUID, email and subscription ID. Each inbound
list read (panel list, the master's node traffic poll, Xray config builds,
bots) repeated that wide join per inbound. Identities now come from one narrow
clients/client_inbounds projection per 400 inbounds, honouring the supplied
transaction; stats output, sibling-traffic append order and the #6436
clients-table source of truth are unchanged.

Closes #6926
2026-10-25 14:01:46 +01:00
VAIO73 be25314157 fix(monitoring): refresh cached public IP addresses (#6933)
The public IPv4/IPv6 lookup was cached for the life of the panel process, so
Overview kept showing an old address after the server's external IP changed,
and a failed first lookup pinned N/A until a restart. Addresses now refresh
every five minutes, a failed IPv4 lookup retries after a minute, failed IPv6
lookups back off 1/2/5/15 minutes, and a temporary failure keeps the last
known address. Status sampling still never waits on the lookup.
2026-10-25 13:50:29 +01:00
YoungReckless4 56ac8d0fc7 feat(amneziawg): add opt-in Linux kernel datapath (#6877)
* feat(amneziawg): add opt-in Linux kernel datapath

Add the kernel backend as an explicit per-inbound option while keeping the embedded backend as the default. Include host preflight, UI/API support, the bundled installer CLI, lifecycle and accounting integration, regression tests, and upstream-facing documentation. Kernel mode is NAT-only and rejects unsupported routing, IPv6, forwarding, and pinned-listen configurations. Local saves check the local host; remote nodes check their own host during apply.

Preserve adopted interface accounting across panel restarts, retain manager state after failed teardown so the job can retry, prevent embedded startup while a kernel interface remains live, and prune collector state for removed peers. Tested the restart accounting path on Debian 13 with XanMod 7.1.11 on x86_64; Docker and cross-kernel coverage are not claimed.

* fix(amneziawg): align readiness and endpoint reporting

Expire endpoint sightings using the latest handshake and sweep stale addresses for the polled interface. Keep the readiness API schema, generated client types, and ready-host empty blocker list consistent; watch the nested form field and defer local readiness checks for remote targets. Document safe rollback before using a build without kernel support.

Regression evidence: the idle endpoint test failed before the collector change because the old address remained visible after 31 minutes; endpoint tests pass after the fix. The form component suite passes 3 tests; restoring the invalid array watch made its off-mode severity test fail, while the corrected dot-path watch passes. Targeted Go regressions across amneziawgkern, job, and service pass.

* style(amneziawg): format readiness test

* test: reproduce kernel AWG client validation gaps

* test(service): cover kernel AWG shared client fanout

* fix(amneziawg): validate kernel client fanout and config values

* test(amneziawgkern): cover owned interface lifecycle recovery

* fix(amneziawgkern): recover owned interfaces on restart

* test(amneziawgkern): retain visibility after recovered cleanup failure

* fix(amneziawgkern): report recovered interfaces while cleanup retries

* style(amneziawgkern): satisfy static analysis

* test(amneziawgkern): cover long rendered peer comments

* fix(amneziawgkern): accept long generated config comments

* test(amneziawgkern): refuse foreign occupied interfaces

Capture the current Ensure ordering failure with real temporary config paths and manager side-effect boundaries.

* test(amneziawgkern): cover stale interface collisions

Extend the Ensure regression across absent links, mismatched live interfaces, cached manager state, and Remove teardown.

* fix(amneziawgkern): verify interface ownership before mutation

Refuse to replace or tear down links unless the canonical config is a valid panel-owned file and its key matches the live interface. Preserve normal adoption and key rotation, and document migration of legacy unmarked configs.

Validation: go test ./internal/amneziawgkern -count=1; go test -race ./internal/amneziawgkern -count=1; golangci-lint run ./internal/amneziawgkern/...; git diff --check.

* build: update Go toolchain to 1.27.3

* test(amneziawg): reproduce unsafe key rotation recovery

Keep the previous owned config authoritative until teardown succeeds, and require fresh managers to rebuild structural key changes. These regressions expose the stuck live-key/disk-key mismatch after teardown failure.

* fix(amneziawg): recover interrupted key rotations

Keep the old owned config on disk until a structural rebuild has confirmed the previous interface is down. Fresh managers only adopt when the live config and desired structural identity match, so retries and removals retain the key needed to prove ownership.

* fix(scanner): align Go image with module version

* fix(docs): pin pnpm release with bootstrap fix

pnpm 12.9.1 restores a Node launcher before its postinstall replaces the target with ELF; pnpm 11 then runs that binary as JavaScript. Pin 12.10.0, which includes the shim fix, so the docs CI bootstrap can switch versions safely.

* fix(xray): build core from pinned module

The configured Xray release asset tag is unpublished, and the latest published binary lacks GetUsersStats used by the panel. Build the packaged core from the exact Xray module version already selected in go.mod for Docker and release targets.

* fix(tests): stabilize local network fixtures

Keep the TUIC stream fragmentation check on a working UDP loopback path,
allocate the AmneziaWG fixture ports dynamically, and wait for a real
Xray stats response before starting API mutation assertions. Recreate
failed readiness channels so an early connection cannot stall the probe.

Verified host and offline parallel Go suites, repeated occupied-port and
UDP tests, race/shuffle runs, and golangci-lint. Production behavior and
dependency versions are unchanged.

* fix(i18n): add the kernel-mode AmneziaWG keys to sk-SK

main gained the sk-SK locale after this branch added its kernel-mode keys
to the other 13, so i18n-dead-keys.test.ts failed on the merged tree.

* refactor(amneziawgkern): apply kernel interfaces with direct commands, no awg-quick

awg-quick evaluates PostUp/PostDown through a root shell, so every value the
panel rendered into its config was one validation slip away from root command
execution. The interface is now built with direct ip/awg/nft execs from a
setconf-only file: link add, awg setconf, address, mtu, an atomically replaced
nftables table per inbound, and the IPv4 forwarding sysctl written directly.

- Panel-owned names and paths: xui-awg<id> interfaces, xui_awg<id> NAT tables
  and configs under the panel's data dir, so nothing collides with an
  operator's own awg0/awg1 awg-quick tunnels in /etc/amnezia/amneziawg.
- FORWARD accepts (comment 3x-ui:xui-awg<id>) when iptables is present: Docker
  and ufw set the FORWARD policy to DROP, which silently black-holed the tunnel.
- A full panel stop records the final counters and tears the interfaces down,
  like the embedded datapath; a stopped panel enforces no quota or expiry.
  Crash survivors are still adopted without a rebuild.
- The job reads counters before reconciling, and from every managed interface,
  so a client disabled for quota, or a removed inbound, is charged for its last
  interval instead of losing it with the dropped peer.
- An unchanged reconcile tick forks no process (it probed the live key with
  `awg show` every 10s); readiness no longer loads the module from a GET.

Verified on the real module (WSL2 6.6 kernel, v3.1.20260906): create, peer and
egress changes in place, SIGKILL adoption with the same ifindex, SIGTERM
teardown leaving no link, table, FORWARD rule or config behind.

* fix(amneziawg): gate kernel host readiness only when a save starts the datapath

Every save of a kernel-mode inbound re-ran the host preflight, so a host that
lost its module (a kernel upgrade without a DKMS rebuild) refused even
disabling the inbound or switching it back to embedded mode. Host blockers now
apply only when the save starts the kernel datapath: a new inbound, a switch
from embedded, or enabling a disabled one. Feature blockers (IPv6, forwarded
ports, pinned listen) still apply to every save.

* feat(nodes): refuse kernel-mode AmneziaWG pushes to nodes that predate it

A node built before kernel mode drops the unknown kernelMode field and serves
the inbound on the embedded datapath while the master's form shows kernel
mode on. Nodes now advertise awg-kernel in X-3x-Node-Caps, and Remote refuses
to push a kernel-mode inbound to a node that has not advertised it, learning
the caps with an inbound list first when this Remote has not heard from it.

* fix(amneziawg): pin the kernel module installer to verified commits

The installer ran as root and built whatever upstream tag sorted newest at
install time. It now clones fixed tags (module v3.1.20260906, tools
v3.1.20260812) and refuses unless each resolves to its recorded commit. The
udp_tunnel perl patch is gone: those releases carry the kernel 7.1.5 compat
upstream, so the patch never matched.

* fix(i18n): translate the kernel-mode AmneziaWG strings

All thirteen non-English locales carried the English text verbatim.

* fix(amneziawg): skip the kernel traffic flush when no kernel interface runs

Shutdown listed the desired AmneziaWG inbounds from the database even on a
panel with no kernel interface, an unneeded query on every stop. A server
stopped before its database was opened panicked on it, which turned
TestServerStopWhenXrayNotRunningSucceeds red whenever it ran first.

---------

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-10-25 13:24:50 +01:00