Commit Graph

3936 Commits

Author SHA1 Message Date
MHSanaei 2e41f908db fix(limit-ip): do not re-add a client the database stopped serving
disconnectClientTemporarily removed an over-limit client from the core
and re-added the snapshot the scan had loaded 100 ms later, so a
disable or detach committed in between was undone: the traffic job's
own RemoveUser had already found nothing, and the disabled account
stayed live in Xray until the next restart.

The re-add now rereads the client and its inbound and skips one that
is disabled, expired, over quota or no longer attached.
2026-11-01 13:22:26 +01:00
MHSanaei cc3aff2434 fix(discord): escape Markdown in alert text the panel did not write
Every Discord alert field went through cleanField, which only trimmed
and truncated, so a login username, a node's error text, a remark or an
email reached Discord as live Markdown: masked links, bold, headings,
mentions. A failed login with a crafted username could post a working
phishing link in the admins' channel.

Foreign text is now Markdown-escaped in every field and command reply,
and values shown in code spans can no longer close their span.
2026-11-01 13:21:08 +01:00
MHSanaei 4d8e2b48ff perf(sub): make balancer member retagging linear
buildBalancerConfig restarted the free tag-suffix search at 2 for every
member, so member k formatted and looked up k tags and n members of one
protocol cost n²/2 lookups; a balancer with many same-protocol members
held a subscription request for minutes.

The search now resumes per base tag where the previous member stopped,
producing the same tags in linear time.
2026-11-01 13:20:27 +01:00
MHSanaei 8bbfda0510 fix(email): cap what one SMTP session reads from the server
net/smtp buffers a server reply of any length, so a mail server, or
anyone on the path before STARTTLS, could send a greeting or EHLO reply
that never ends and grow the panel's heap until the 30 s deadline, on
every notification.

Each session now reads at most 1 MiB from the server, below TLS, and
fails with a clear error past it.
2026-11-01 13:18:54 +01:00
MHSanaei 06cb1742a1 fix(server): bound the public-IP lookup and accept only an address
getPublicIP read a third-party IP echo service's body whole and cached
whatever came back as the panel's address, so one misbehaving service
could make the panel inflate an endless response at startup, and an
error page became the address shown in every status sample.

The read is capped at 64 bytes and only a parsable IP is accepted;
anything else counts as unavailable. The GitHub release lookups are
capped as well.
2026-11-01 13:18:01 +01:00
MHSanaei f99c6c664d fix(sub): resolve legacy socket fallback fronts on PostgreSQL too
The socket-front lookup filtered and extracted settings.fallbacks with
SQLite's json_valid, JSON_TYPE and json_extract, which PostgreSQL does
not have, so on PostgreSQL the statement failed and a legacy @socket
child never got its front's address and security in its links.

The lookup now selects the inbounds whose settings mention fallbacks and
parses them in Go, which works on both databases and still skips an
inbound whose settings do not decode.
2026-11-01 13:16:14 +01:00
MHSanaei 93d1e63554 fix(node): count only values against the node-answer value cap
decodeCappedValues is documented to count JSON values, but it also
counted object keys and closing brackets, so a node's GUID-keyed online
and IP trees reached the cap at roughly half the stated number of
values, and the master then refused the whole report rather than the
excess.

Keys and closing brackets no longer count; every value, nested ones
included, still does.
2026-11-01 13:14:46 +01:00
MHSanaei 1d373cf67e fix(node): let a node take back a client orphaned before the upgrade
The migration backfills sync_orphaned_by = 0 for every client a node
merge had already soft-orphaned, and the gate refused a detached record
unless its mark named the reporting node. A client inside its orphan
grace window at upgrade time was therefore refused when its node
reported it again, and ReapSyncOrphans then deleted it on the master
while the node kept serving it.

A mark that names no node predates the column and may be taken back by
any node, as before.
2026-11-01 13:13:10 +01:00
MHSanaei 828855cdb5 fix(node): apply the client gate to emails with non-ASCII capitals
The node client gate loaded the records it judges with LOWER(email) IN
the lowered reported emails, but SQLite's LOWER folds ASCII only. For a
stored email such as Иван no record was found, so the node's claim was
treated as a new client and passed untouched, rewriting a client that
lives only on the master's inbounds.

The lookup matches the lowered or the stored spellings, as the shared
email checks already do.
2026-11-01 13:12:15 +01:00
MHSanaei 4cddcff12b fix(sub): fetch operator-chosen private and proxied external subscriptions again
Guarding the external subscription fetch against a provider's redirects
also refused the URL the operator configured when it names a LAN or
loopback address, and the guarded transport dropped the host's proxy
setting, so a panel merging another panel on its own network, or one
reaching the Internet only through a proxy, silently lost those links.
A provider listing more than 1000 links was refused whole as well.

A URL that names a private address itself is now fetched, while a
hostname still is not, since the provider controls what its DNS answers;
public providers stay held to public addresses through every redirect.
A fetch the host's proxy setting covers goes through that proxy, and a
long list is cut to its first 1000 links instead of refused.
2026-11-01 10:30:26 +01:00
MHSanaei 255c658468 fix(sub): escape a VLESS id the way the panel's own links do
Subscription links escaped the VLESS id with url.QueryEscape, while the
panel builds its own link and QR through the browser's URL parser,
which escapes userinfo with the WHATWG userinfo set. A non-UUID id such
as team+1 was therefore served as team%2B1 by the subscription and as
team+1 in the QR, and a client that does not percent-decode userinfo
authenticated with a different id depending on where it came from.

The id is now escaped with exactly that set, which still keeps a line
break or an "@" from splitting the link.
2026-11-01 10:26:54 +01:00
MHSanaei 0394996a30 fix(node): keep summing usage across panels for a client the node shares by email
The client gate refused a node's entry for an email the master serves
only on its own or another node's inbounds, and so also dropped the
node's usage for it; on top of that, a refused or rewritten entry made
the master mark the node dirty and push its own settings back, which
deleted the node's local client of that email. A master → node → node
chain whose panels each serve the same client therefore stopped adding
the nodes' usage to the master's totals.

Such an email is now usage-only: the node's usage still adds to the
master's row, while its enable, expiry and record values do not. A
claim rewrite no longer forces a re-push; the stored, filtered settings
are what the reconcile fingerprint tracks.
2026-11-01 10:25:14 +01:00
MHSanaei 9c0bf6938c test(sub): leave the decode-once allocation bound room for the race detector
TestSubscriptionDecodesInboundSettingsOncePerRequest capped a request
at 128 MiB, which the race job exceeds on correct code (up to 170 MiB)
because the detector inflates allocations. One decode per link costs
489 to 750 MiB without it, so a 256 MiB bound still tells the two apart.
2026-11-01 10:10:01 +01:00
MHSanaei 5c6cbb5680 chore(deps): update follow-redirects to 1.16.2
A high-severity advisory published against follow-redirects 1.16.0 to
1.16.1, pulled in through swagger-ui-react's client, fails the
frontend's production audit. npm audit fix moves it to 1.16.2, along
with the swagger-ui-react 5.34.1 patch release that allows it.
2026-11-01 10:04:45 +01:00
MHSanaei 082d7ac091 fix(xray): block private-range egress in per-peer AmneziaWG IPv6 outbounds
The per-peer IPv6 freedom outbounds the panel injects for AmneziaWG
clients were built with empty settings and their routing rule sits
ahead of the template's private-range block, so a peer's IPv6 traffic
skipped both layers and could reach fc00::/7 hosts on the panel's
network.

Each per-peer outbound now carries the same private-range finalRules
block every other freedom outbound gets.
2026-11-01 10:03:17 +01:00
MHSanaei ecd817658c fix(amneziawg): attribute an equal-prefix source to the peer the device picks
PeerIndex names the peer a source address came from, but for two peers
holding the same prefix, which only a default route can be after the
overlap check, it kept the first while amneziawg-go's allowed-IPs table
hands the prefix to the peer configured last. Traffic was then billed
to a different client than the one the device accepted it from.

An equal-length match now goes to the later peer, as in the device.
2026-11-01 10:02:11 +01:00
MHSanaei efaa73db3a perf(tgbot): stop loading the whole inbound per client in usage listings
Listing a Telegram account's clients ran CheckIsEnabledByEmail and
describeAttachedInbounds for every client, and both loaded and parsed
the client's entire inbound, so one message cost the number of clients
times the size of their inbound's settings.

CheckIsEnabledByEmail now reads the client's own record first, keeping
the inbound parse only for legacy clients never normalized into the
clients table, and describeAttachedInbounds takes every remark in one
query without loading any settings.
2026-11-01 09:14:43 +01:00
MHSanaei 7163c6e521 perf(sub): decode TUIC and AmneziaWG settings once per request
genTuicLink and genAmneziaWGLink, and their Clash and JSON siblings,
parsed the inbound's whole settings, every client included, for each
link they built, bypassing the per-request caches the other generators
use. Building the links of n clients of one inbound, as the export of
all links does, cost n full parses of an n-client document.

Typed per-request caches next to settingsByInbound now hold the decoded
TUIC instance, indexed by email, and the AmneziaWG server block, and
every generator reads through them.
2026-11-01 09:09:28 +01:00
MHSanaei dda44c1e72 perf(sub): resolve legacy socket fallback fronts once per request
projectThroughFallbackMaster found the front of each unix-socket child
by scanning every inbound's settings JSON, once per child, so a
subscription cost the number of socket inbounds times the size of all
settings in the table.

A request now collects every host's socket fronts in one statement,
the lowest-id front naming a socket on that host still wins, and an
inbound with unreadable settings no longer fails the lookup.
2026-11-01 09:06:32 +01:00
MHSanaei 878d37c2fc fix(panel): bound the sponsor logo cache and evict logos that left the list
The logo cache was keyed by whatever names the sponsors list named and
never pruned, so a list naming thousands of logos made the public logo
route pin hundreds of megabytes, and every hourly refresh with new
names added more.

Only the first 32 distinct logo names of the active list are kept,
surplus sponsors render with their initial as a bad logo name already
does, and each store drops cached logos no longer on the list.
2026-11-01 09:03:42 +01:00
MHSanaei 1507fd4edf fix(hwid): re-check the device limit where an unlimited fetch registers
EnforceHwidForSubID read the subscription's device limit and, finding
none, recorded the device without looking again. A fetch racing an
operator who had just set a limit could therefore register its device
right after the limit was written and trimmed, leaving more devices
registered than the limit and serving all of them.

The tracking now re-reads the limit and writes the row in one
transaction, which serializes with the operator's, and a limit that
has appeared sends the request through the limited gate instead.
2026-11-01 09:02:15 +01:00
MHSanaei fac9009398 fix(tgbot): escape template parameters once in I18nBot
Every bot message is sent HTML-parsed while the intended markup lives
in the translation templates, yet the values filled into them were
escaped only at some call sites. Remarks and e-mails, which a node
chooses for the inbounds it reports, reached the scheduled report and
the client info unescaped and turned into markup, and the reports that
escaped their whole text instead showed the templates' own tags.

I18nBot now escapes every parameter value, the call sites drop their
own escaping and the whole-report escapes, the two values concatenated
outside a template are escaped where they are written, and callback
answers, which are plain text, are unescaped again.
2026-11-01 08:58:43 +01:00
MHSanaei a9191cb468 fix(amneziawg): let an emptied forwardedPorts withdraw a client's forwards
UpdateInboundClient carried the stored forwardedPorts forward whenever
the incoming value was empty, and Client.ForwardedPorts was a plain
omitempty string, so clearing the field in the client editor, as its
hint says to, reported success while the settings kept the old ports
and the host kept relaying them to the client's device.

The field is now a pointer, as KeepAlive is: nil (not sent) keeps the
stored spec and "" withdraws it. The client record follows the live
spec on every update, and a one-time seeder fills records that still
hold their creation value from the inbound settings, so the editor no
longer posts an empty field that would drop forwards added later.
2026-11-01 08:55:38 +01:00
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 2024d34928 fix(geodata): index a split category's attributes in one pass
scanIndex merged each further record of an already-seen category with
mergeAttributes, which rebuilt a map and re-sorted every attribute the
category had so far. A category with A attributes split over N records
cost A*N, about an hour and a half for a 5 MB file, while the scan lock
was held and every geodata route waited on it.

Each category now collects its attributes in one set during the walk,
and the sorted lists are built once at the end.
2026-11-01 08:33:19 +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 6dbd219a14 fix(database): keep disabled clients disabled in the ClientsTable seeder
The one-time ClientsTable migration builds client records from each
inbound's settings with a plain Create, and gorm's default:true on
ClientRecord.Enable drops an explicit false from that insert. Every
client an operator had disabled before v3.1.0 was therefore stored
enabled on an in-place upgrade or a restored backup, and Xray, which now
reads that column, let the customer back in.

The seeder restates enable=false after Create, as the service paths do
since #6478, and a legacy client whose settings carry no switch stays
enabled, as releases before 3.1.0 served it.
2026-11-01 01:16:56 +01:00
MHSanaei 2467b9d542 fix(sub): bound what one subscription response renders
A subscription's size is the product of its clients and inbounds with
each one's remark and endpoints, and nothing bounded that product: data
adopted from a node could make one unauthenticated request render
gigabytes and run the panel out of memory.

Each request now carries an 8 MiB budget that the raw, JSON and Clash
outputs charge as they materialize a client's links, configs or
proxies. Rendering stops once it is spent, with one warning, and
anything below the budget is unchanged.
2026-11-01 01:15:28 +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 3526a65f5d fix(web): bound the panel's open connections and share asset gzip
The engine-wide gzip middleware gave every in-flight request its own
compressor of about 1 MiB, and the public /assets bundles let an
anonymous client keep a handler blocked in Write until WriteTimeout, so
a thousand stalled downloads held a gigabyte. The listener itself took
any number of connections, each holding buffers and a goroutine before
any login.

The bundles are now served from a gzip copy made once per file, with
the assets prefix left out of the per-request middleware, and the panel
listener closes a new connection once 2048 are open or its source (an
IPv4 address or IPv6 /64) holds 256; a loopback reverse proxy counts
only toward the total.
2026-11-01 01:01:38 +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 c2250dbe57 fix(sub): keep the salamander warning state one entry per inbound
genHysteriaLink remembered which salamander settings it had warned
about in a set keyed by the setting names themselves, which a node
chooses with no bound on length or number and the set never forgot,
and it logged the names verbatim into the in-memory log buffer. A node
renaming a setting on every poll grew the master's memory by twice the
name's size per subscription fetch until the process ran out.

The set is keyed by inbound id and holds a digest of the names, the
warning repeats only when that set changes, and it logs at most eight
names cut to 32 characters.
2026-11-01 00:45:21 +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 68e06a174b fix(job): withhold fail2ban bans for addresses a datagram alone reports
The IP-limit job treated every address on the core's online list as one
its sender proved to own and wrote the oldest over-limit ones for the
3x-ipl jail. For Shadowsocks over UDP and for mKCP the core lists a
datagram's source field with no exchange behind it, so a client with an
IP limit could forge sources and get any address firewalled.

A client attached to an enabled inbound that accepts such datagrams,
on the master or on a node, is still counted and disconnected, but its
old addresses are no longer written for fail2ban; a warning says why.
2026-11-01 00:39:26 +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 c0bd572085 fix(sub): look up a socket child's fallback front on the child's own host
projectThroughFallbackMaster found the front of a unix-socket child by
scanning every inbound's settings.fallbacks for that socket. A node's
inbound settings are mirrored from the node, so a node could name any
socket and have its own port, security and external proxies substituted
into the subscription links of another host's child.

A socket exists only on the host that binds it, so the lookup, and the
same scan the inbounds page uses for share links, now considers only
inbounds hosted where the child runs.
2026-11-01 00:21:03 +01:00
MHSanaei bc2c64d901 fix(amneziawg): bound the egress relay's pending tunnel DNS lookups
relayUDP started a goroutine for every domain-addressed datagram and
each lookup opened its own netstack UDP endpoint, so the lookups in
flight were bounded only by the client's send rate: one proxy user could
grow the panel by gigabytes and exhaust the outbound stack's ports,
stalling every user of that outbound.

One association may now have 32 datagrams waiting on DNS and drops the
rest, a cached name is delivered from the reader loop without a lookup,
and resolveTunnelVia takes one of 1024 process-wide slots before it
opens an endpoint, which also covers the TCP CONNECT path.
2026-11-01 00:19:34 +01:00
MHSanaei 1810996db6 fix(amneziawg): stop the TUN handoff from parking a peer's receiver
stackTun.WriteNotify runs on whichever goroutine made the stack emit a
packet; for echo replies and RSTs that is amneziawg-go's per-peer
receiver. It waited without bound for room in the outbound queue, whose
reader waits on that same receiver while the device rebinds, so a peer
that flooded the stack while nothing drained the queue deadlocked the
next reconfiguration of the interface.

The handoff now tries once, then waits at most 5 ms and drops the
packet, as channel.Endpoint already does when its own queue is full.
2026-11-01 00:17:33 +01:00