The race job failed in TestAudit3ManagerEnsureActualSendersWithPersistentTraffic
with "close called for canceled stream 14". The server parses one command
per uni stream and then calls CancelRead, as the quinn reference server does
on drop, so its STOP_SENDING can reach the client before the client's own
Close and quic-go reports that Close as an error. The data was already read.
Every test that wrote a command on a uni stream and required Close to
succeed shared this race. closeUniStream accepts only a remote StreamError
on the stream's context, so any other Close failure still fails the test.
The race job failed on TestServerUDPDatagramE2E with Up:0 Down:1300.
BytesUp is added on the sending goroutine after the relay Send returns,
while BytesDown is added on the response goroutine, so the mock echo can
be counted and delivered before the upload is. The test drained the
counters once right after the reply and assumed both were present.
Production is unaffected: deltas left for the next collection window are
still summed. The TCP E2E test made the same assumption, so both now
accumulate drained deltas until up and down reach the payload size.
The remote certificate fetch now dials through the same netsafe guard
as the REALITY target scan. A private or loopback endpoint is refused
unless the request carries allowPrivate; the inbound form asks the
operator to confirm and retries with the opt-in.
A SQL dump replay only has to rebuild the tables of the database it
restores into. Run it on a single connection whose attached-database
limit is zero, so the script cannot open or create any other file.
* feat(tuic): implement native in-process Go TUIC v5 server
- Implement native TUIC v5 protocol server on pure Go using quic-go
- Bridge decrypted TCP/UDP traffic into Xray-core via loopback SOCKS5 inbound
- Support full Xray routing rules (geosite/geoip) and cascading outbounds
- Implement atomic per-client traffic accounting with TotalGB and ExpiryTime
- Add automatic legacy cleanup for older Rust tuic-server binaries, configs, and orphaned processes
- Eliminate external Rust tuic-server downloads from install/CI scripts
* fix(tuic): address traffic accounting, client reload, and socket lifecycle issues
* fix(service): update checkTuicSocksReverseConflict to use bindAddr for listenOverlaps
* fix(tuic): resolve traffic double-accounting, UDP fragmentation, and socket lifecycle issues
* feat(tuic): complete native Go integration and address audit findings
- Integrate an isolated QUIC fork pinned to a specific commit
- Preserve original QUIC dependencies for Xray, Hysteria and Gin
- Apply BBR, CUBIC and Reno to server connections and exported client profiles
- Bridge Xray BBR with correct monotonic time and congestion type conversions
- Handle congestion sender recreation after PMTU changes
- Update congestion control for new connections without restarting the listener
- Preserve existing connections and their selected congestion controller
- Apply per-inbound log levels through the shared panel logger
- Add lifecycle, authentication and TCP/UDP relay events without exposing secrets
- Rate-limit repeated authentication and relay warnings
- Support native and QUIC UDP relay modes on the same listener
- Recover UDP associations after relay worker failures
- Fix TCP relay cancellation, idle shutdown and half-close handling
- Close active sessions when client credentials are revoked or disabled
- Track traffic by immutable client statistics IDs across email and UUID changes
- Prevent ambiguous accounting and duplicate UUIDs within TUIC inbounds
- Persist pending traffic in a durable shutdown journal
- Replay journal batches transactionally without duplicate accounting
- Report server shutdown failures through the shared logger
- Preserve legacy flat and nested TUIC settings compatibility
- Normalize congestion controller values consistently across backend and frontend
- Preserve controller, UDP mode and SNI in client links and subscriptions
- Separate client profile options from server settings in the TUIC form
- Keep certificate path autofill explicit when changing client SNI
- Align UDP packet size validation with protocol limits
- Simplify and localize TUIC field hints and certificate autofill messages
- Add controller, TCP/UDP, logging and live settings update tests
- Add accounting identity, journal replay and shutdown regression tests
- Add relay recovery, session revocation and legacy frontend form tests
* fix(service): alias the TUIC duplicate-UUID subquery for PostgreSQL < 16
syncInboundClients runs a COUNT(*) FROM (subquery) for every client sync,
whatever the protocol. PostgreSQL before 16 rejects a FROM subquery with
no alias, so on the distro PostgreSQL install.sh provisions (14 on Ubuntu
22.04, 15 on Debian 12) every client add or edit failed with SQLSTATE
42601. Reproduced against postgres:15 with the new env-gated test.
* fix(database): create tuic_traffic_receipts through the model migration
AddTuicTrafficBatch issued CREATE TABLE IF NOT EXISTS at runtime, a schema
change outside db.go. The table was invisible to allModels and
migrationModels, so x-ui migrate-db dropped the receipts and a retained
journal could be counted twice after a SQLite to PostgreSQL move. It is
now a GORM model in both lists, and the insert uses OnConflict DoNothing.
* chore(tuic): skip the ICMP-dependent relay test on Windows, drop dead collectors
Go disables SIO_UDP_CONNRESET on Windows, so a dead UDP bridge never fails
a read there and TestAudit3UDPAssociationMustRecoverAfterBridgeReadFailure
was red on every Windows run. Server.CollectTotalTraffic and
Manager.CollectTraffic had no caller.
* refactor(tuic): serve TUIC on apernet/quic-go instead of a personal fork
The native server depended on github.com/poise52/quic-go, a personal fork
of apernet/quic-go patched only to pick the congestion controller before
the handshake. That put a second QUIC/TLS stack in the binary that no
upstream security fix reaches. apernet/quic-go is already in the graph
through xray-core and exposes SetCongestionControl, so BBR is now installed
on each accepted connection with Xray's own congestion.UseBBR; the
cross-module BBR adapter is gone.
apernet ships New Reno as its only built-in sender, so a cubic setting is
served as new_reno server-side (clients still get cubic in their profile).
The test inspectors now read the sender under congestionMutex, which the
post-handshake install writes under.
Linux loopback, single stream through Xray: 2428 -> 3383 Mbit/s (bbr).
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
The bot read the panel-egress bridge once at start, so when Xray came up
after the bot (or Panel Outbound was set later) it kept dialing Telegram
directly until restarted - on a filtered host it never connected.
With no dedicated bot proxy, the fasthttp client now resolves the bridge on
every new connection and falls back to a direct dial when it is absent.
Raised in #6682.
* Write config.json after a hot apply
tryHotApply only updated the in-memory snapshot, so bin/config.json stayed
stale until the next cold start and the Telegram/Discord config backups
uploaded old rules. Persist the new config once the API calls succeed; a
write failure is logged and does not restart the running core.
* Test that a hot apply refreshes config.json
* Match non-ASCII capitals in the clients group filter and search
SQLite LOWER() only folds ASCII, so a group or search term with a Cyrillic,
Persian or other non-ASCII capital never matched after being lower-cased in
Go. Also compare the value as typed.
* Match lower, upper and title-case spellings for non-ASCII search and group filter
* Bound the syslog view with a journalctl timeout
* fix(syslog): bound the journal scan window and soften the timeout message
Limit journalctl to the last 30 days so a rare -p level cannot scan the whole
journal, and drop the guessed cause and the host-wide vacuum advice from the
timeout message.
* fix(syslog): drop --since from the journalctl call and test the timeout
On systemd 249/252 (Ubuntu 22.04, Debian 12) journalctl seeks to --since
and reads forward when both --since and -n are given, so the Syslog view
showed the oldest 200 lines of the 30-day window instead of the newest.
Reproduced in debian:12, ubuntu:22.04 and ubuntu:24.04 containers on a
synthetic journal; only 255 kept the newest lines. The window also bought
nothing: on a 340 MB journal every variant (with or without --since, rare
-p level or not) answered in ~10 ms on 252 and 255.
Keep the 15s deadline as the guard against a stalled journalctl and cover
it with a fake journalctl on PATH; the args test pinned the broken flag
and is removed.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* fix(runtime): reset node inbound traffic by node-side id
Remote.ResetInboundTraffic posted to the master's inbound id (ib.Id),
while every other node-side inbound call resolves the id assigned by the
node from the tag. The reset therefore hit an unrelated inbound or failed
silently (warning only), so the panel reported success either way.
Resolve the id through resolveRemoteID(ctx, ib.Tag) and fail before
posting when the tag cannot be resolved.
Fixes#6713
* fix(job): list the inbound on the periodic-reset fake nodes
Remote.ResetInboundTraffic now resolves the node-side id from the tag via
panel/api/inbounds/list before posting resetTraffic. The fake nodes in
periodic_traffic_reset_nodes_test.go answered that list with no obj, so the
tag never resolved, no reset reached a node, and
TestPeriodicResetReachesInboundNodesConcurrently failed (green on main, red
after merging the node-side-id fix). Each fake node now lists the inbound it
hosts, so the test again measures concurrent resets against a node shaped
like a real one.
---------
Co-authored-by: Кот <kot@zeroclaw.local>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* fix(sub): keep serverNames out of a reality host's JSON client config
A host with an SNI on a REALITY inbound set both serverName and the
server-side serverNames list on the per-host stream. Both the JSON and
Clash renderers have already reduced the stream to its client form by
then, so serverNames was never read, and the JSON subscription shipped
it in the proxy outbound. xray-core refuses to start that config
("non-empty serverNames, please use serverName instead"), which breaks
every JSON-subscription client on the inbound. Set serverName only.
Fixes#6690
* docs(sub): keep the host reality SNI comments within two lines
Same two-line comment cap the #6694 review applied.
* fix(sub): keep the spider settings in a reality spiderX seed's query
xray's REALITY client reads p, c, t, i and r from the spiderX query as
its spider's own settings (padding, concurrency, times, interval,
return). deriveSpiderX hashes the whole seed into a bare /path per
client, so any query set on the inbound was dropped from every share
link and JSON subscription, and the spider always ran with defaults.
Keep the seed's query after the derived path. The hash input is
unchanged, so every existing client's spx stays the same; only seeds
that carry a query gain it. The frontend mirror and the cross-language
vectors are updated together.
* docs(sub): keep the deriveSpiderX comments within two lines
Review feedback on #6694: the added lines pushed both doc blocks past the two-line cap.
The node gate assumed a node-assigned sidecar row would never converge,
because the master's reconcile loops only read node_id IS NULL rows. But
a pushed row is local on the node's own panel, whose AmneziaWG, TUIC and
mtg loops run it like any other; node-adopted rows of these protocols
already worked. Only creating or cloning them from the master was blocked.
Open the three protocols on both lists and fix what assumed the master's
host for a node row:
- A node older than the release that introduced the protocol (MTProto
v3.5.0, AmneziaWG v3.7.0, TUIC v3.8.0) would hand it to Xray as-is, so
add and protocol-changing update refuse it, and a node that has not
reported its version yet. Dev builds ("dev+<sha>") track main and pass.
- MTProto's routeXrayPort is a loopback port on the host running mtg.
The master no longer allocates one, nor forwards a cloned source's, for
a node row; the node allocates its own and node sync adopts it back.
- AmneziaWG forwardedPorts were checked against the master's inbounds,
web port and id-derived relay ports. A node row is now checked against
its own node's inbounds; the node re-checks what only it knows.
UpdateInbound restores the stored nodeId before that check.
Verified on a docker master+node pair: AWG, routed MTProto and TUIC
created and cloned from the master start on the node (interface, mtg,
tuic-server); an AWG client added and an MTProto client edited on the
master apply on the node; the node-chosen egress port survives edits.
Closes#6306
.nvmrc and frontend/package.json engines moved to Node 26 / npm 11 and the
lockfile is now written by npm 11, but the Dockerfile's frontend stage stayed
on node:22-alpine. npm 10 rejects that lockfile ("Missing: msw@2.15.0 from
lock file"), so `npm ci` fails and the image no longer builds.
Windows Firewall prompted on every run of amneziawgnet.test.exe, since
the tests opened AmneziaWG, outbound-client and port-forward sockets on
all interfaces and go test rebuilds the binary under a fresh temp path,
so a granted exception never sticks.
Route every "all interfaces" bind through wildcardBindHost, which is
empty in production (behaviour unchanged) and pinned to 127.0.0.1 by
the package TestMain.
The node-assignment path in the service package needs the same
MAJOR.MINOR.PATCH comparison the panel updater uses, but service/panel
imports service, so the helper cannot stay there.
* fix(api-token): keep a token's scope when the CLI regenerates it
RecreateByName deleted the named row and created a new one without a Scope,
so the insert took the column default of admin. Since -tokenName lets the CLI
regenerate any token, rotating a monitor or node-sync token silently turned it
into a full-access one.
The replacement now takes the scope of the row it replaces, and a new name
still gets admin as before. A stored scope this build does not know, as after
a downgrade, fails the rotation and leaves the row alone instead of guessing.
Assisted-by: Claude Code:claude-opus-5-5 (mostly)
* feat(cli): let -getApiToken choose the scope of the token it issues
-tokenScope sets the scope on both branches of -getApiToken: the token minted
on a fresh panel and the one regenerated on a populated panel. Without the flag
a regenerated token keeps its scope and a new one gets admin, so every existing
invocation, install.sh included, behaves as before.
An unknown scope is refused before anything is deleted, so a typo cannot
revoke the token it meant to rotate.
Assisted-by: Claude Code:claude-opus-5-5 (mostly)
* fix(api-token): keep a token's expiry when the CLI regenerates it
RecreateByName built the replacement row with ExpiresAt 0, so running
`x-ui setting -getApiToken -tokenName <name>` on a token issued through
the API with a deadline handed back one that never expires, and said
nothing about it - the same silent widening this branch fixed for scope.
The replacement now carries the replaced row's ExpiresAt. A token whose
deadline has already passed is refused instead of rotated, since keeping
the deadline would mint a dead token and dropping it would revive an
expired credential without limit; the expired row is left untouched.
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
* Fix fragment exports for older Xray clients
* fix(link): tolerate a finalmask without tcp in panel share links
withLegacyFragmentRanges called finalmask.tcp.map unguarded, but stored
rows reach the link generator unparsed and dropEmptyFinalMask deletes an
empty tcp list on save. Any VMess/VLESS/Trojan/SS inbound with only UDP
masks or quicParams threw a TypeError in the QR, info and export-links
views. The Go counterpart already skipped a missing tcp.
Also trims the Go helper's comment to the two-line cap and drops a
[]string branch no JSON-decoded finalmask can reach.
---------
Co-authored-by: Artem K <a.kush@vkteam.ru>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
The bot never read the replies under its own findings, so a finding a
maintainer had already declined came back on the next `@claude review`.
It now reads every comment and inline thread first: a maintainer's answer
settles a finding for good, anyone else's is a claim checked against the
code, and the summary gives each earlier finding a disposition. It also
reads the issue the PR claims to fix and reports a partial fix.
REVIEW.md asks for an upstream symbol behind every wire-format claim, but
the job had no xray-core source (#6718's review said so). The module the
base go.mod pins is now unpacked into a hidden dir in the base workspace;
nothing from pr-head runs.
REVIEW.md gains the rules only /senior-review carried: keep read,
reproduced and inferred claims apart, evidence for performance findings,
a traced trust boundary for security ones, and duplicated logic as a
finding. The summary now says each inline finding in one line.
65b9bfed narrowed the fix carve-out to "one clause naming WHERE the fix
belongs", but the bot still closes every finding with that clause, and set
beside the defect it already named, the location is the fix. On #6718 it
listed the two capabilities the bounding set lacks, then wrote "The fix
belongs in the capability bounding set". Drop the carve-out from REVIEW.md
and the workflow prompt: the finding's file:line already says where.
* fix(sub): carry REALITY ML-KEM hint in VLESS links
Keep raw share links in parity with Clash subscriptions for Xray 26.9.8+. Preserve the URI hint through Go and frontend imports, expose it in the outbound editor, and update the documentation tooling.
* fix(link): accept REALITY ML-KEM boolean aliases
* test(frontend): isolate Happ preset notifications
* fix(link): keep the ML-KEM hint out of Xray REALITY settings
support-x25519mlkem768 is a Mihomo reality-opts option; xray-core's
REALITYConfig (infra/conf/transport_security.go) has no such field and its
JSON loader drops unknown keys silently. The PR also stored it as
realitySettings.supportX25519Mlkem768 in Xray outbounds (form switch, Go and
TS link import, docs outbound builders) and as an inbound settings default
that is stripped before Xray and read by no link generator. The outbound
switch therefore did nothing, and imported links carried a dead key into the
JSON subscription.
The share-link hint itself stays: Go, frontend and docs still emit
support-x25519mlkem768=true on VLESS REALITY links and drop it on a TLS host
override.
---------
Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
CodeQL (go/allocation-size-overflow, alerts #115/#116) flagged the
len(rawDomains)+len(rawResolvers) capacity hint. The sum cannot overflow
in practice, but the hint bought nothing: resolver-derived domains are
deduplicated and append grows the slice as needed.
Refresh Go, frontend, and documentation dependencies, including MSW 3 and pnpm 12.8.1. Update the MSW test setup to use the renamed `onUnhandledFrame` option.
Bump xtls/xray-core to b26a91de4f (v26.9.30) and the three binary pins
(DockerInit.sh, release.yml Linux + Windows) in lockstep. No deleted
symbols; the sing and sing-shadowsocks indirect deps drop out with the
SS2022 rewrite.
XDNS finalmask (#6718) replaced its string lists with objects: domains
are {name, types, edns0, lenLimit, labelLimit} and client resolvers
{type, settings.addr}. The loader no longer parses the old lists, so a
single stored xdns mask keeps the whole core from starting. The new leaf
package internal/util/maskcompat converts them: "name[:type]" becomes a
domain and "name[:type]+udp://addr" a domain plus a udp resolver. A bare
name maps to TXT, the type legacy clients queried by default, and each
converted domain keeps the 1232-byte EDNS0 the old code always used
(without it the server caps answers at 512). It runs from:
- the XdnsFinalmaskObjectsFix seeder, over inbound streams, hosts, the
xray template, the global sub-JSON mask and cached subscription
outbounds;
- inbound save (normalizeStreamSettings) and GetXrayConfig, for rows
that never went through the seeder;
- both link importers, since fm= from an older panel carries the lists.
The finalmask form edits the object shape (every key needs a registered
field, or the finalmask watch drops it on save) and lifts legacy masks
on open. The udp-mask golden fixture moves to the object shape, which
TestGoldenStreamFixturesBuildInXray now builds through the core. The
wire format changed as well, so pre-upgrade clients need the new core.
WireGuard outbound (#6771) dropped settings.domainStrategy and the
remoteDNS "local" mode. The endpoint lookup now follows
sockopt.domainStrategy and in-tunnel targets the outbound's
targetStrategy. The old key is silently ignored, which undid the
IPv4-first endpoint lookup the WARP outbound depends on (#5205), and
"local" now panics the core at startup because remoteDNS goes through
netip.MustParseAddr. The WireguardDomainStrategyFix seeder moves a stored
family preference to both keys (a value already set wins) and turns
"local" into targetStrategy; the outbound form lifts legacy rows the same
way and drops its select, the WARP modal writes the new placement, and
the inbound form loses a field the server never read.
ValidateOutboundConfig now refuses a non-IP remoteDNS entry, which
conf.Build() lets through, on template save and for outbound
subscriptions.
Noise finalmask items accept type "exp" (#6862), a tag expression. The
form offers it for noise items only: header-custom items go through the
core's PraseByteSlice, which refuses it.
TUN gained autoSystemDnsToGateway (Linux) and autoSystemWfpBlockLeak
(Windows). Both pass through the settings schema so a value set in JSON
survives the next form save.
MASQUE (inbound, outbound, transport) and the XDRIVE transport are new
protocols the panel does not offer yet; their new loader refusals only
cover configs the panel never generates. The FakeDNS IPv6 pool default,
the SS2022 rewrite (same gRPC account; emails are now deduped
case-insensitively, as the panel already does), the restored udphop
interval default and the rest change no panel-facing config.
FileManager typed every download text/plain. Android's MediaStore appends
the MIME type's extension whenever the name's own extension maps elsewhere,
so a subscriber saving a WireGuard config got peer.conf.txt, which the
WireGuard app refuses; .json, .yaml and .log downloads were renamed the same
way. Desktop browsers honour the download name, which is why only phones
saw it. application/octet-stream carries no extension of its own, so the
name the panel chose is kept.
Invariant: an inbound's enabled Hosts are the endpoints every client config
for it advertises, whichever surface renders that config.
WireGuard and AmneziaWG broke it. Their raw generators ignored the
externalProxy entries Hosts are injected as and always emitted
resolveInboundAddress, so the raw subscription, the sub page .conf, the
clients links API and "export all links" gave out the panel address while
the JSON and Clash formats of the same inbound used the Host.
advertisedEndpoints now states the fan-out once for mtproto, wireguard and
amneziawg.
The browser-built configs had the same gap. The Clients page WireGuard and
AmneziaWG config blocks and QR panels, and its TUIC Clash config, used the
panel hostname next to server links that already used Hosts; the Inbounds
page peer configs, QR and export ignored them too. withMtprotoHostEndpoints
becomes withHostEndpoints over a shared hostEndpointsFor mirror of the
backend, the tunnel fan-outs render one config per Host, and the clients
page waits for the hosts list the way the inbounds page does, so an empty
list means "no hosts" rather than "not loaded yet".
Invariant: saving an inbound's configuration never changes which clients it
holds nor whether it is enabled; both have their own endpoints. The edit
modal posts back the clients and the enable flag it loaded when it opened.
A client added meanwhile (another admin, the bot, the API, LDAP) was
detached and its stats deleted; a client deleted meanwhile came back with
its credentials, restoring access that had been revoked; an inbound
switched off meanwhile was switched back on.
For every save but a master's node-sync push, UpdateInbound now takes the
client list and enable from the row it re-reads inside the writer; this
replaces the lifecycle-only carry from the previous commit. Client
validation (renewal schedule, Hysteria auth, TUIC credentials) moves after
that swap so it judges the clients actually saved: a protocol switch keeps
the stored clients, and #6268's refusal must apply to them.
The edit form no longer loads or sends clients, so neither the JSON editor
nor validation sees a copy the server ignores, and the enable switch shows
only when adding; the list toggle (/setEnable) covers existing inbounds.
Tests that added or re-keyed clients through a panel inbound save pinned
the old rule; they now drive the master-push path, where payload clients
still apply.
Invariant: the sidecar's quota counter for a client is zeroed exactly when
the panel zeroes that client's usage. InboundService.ResetAllTraffics
resets only inbound counters, yet it cleared every MTProto client's
sidecar quota - on the master and, through its node propagation, on every
node - handing out a fresh quota while the panel still counted the old
usage. ResetAllClientTraffics for one inbound likewise cleared the quotas
of MTProto clients on every other inbound.
The inbound-level reset no longer touches sidecar quotas, and the
per-inbound client reset zeroes only the clients it reset.
Invariant: a client traffic reset zeroes every counter that enforces the
client's quota - the master's, each hosting node's, and the local MTProto
sidecar's. A node cuts a client on its own local counters, and the master
adopts that verdict when both judged the same limits (#4917).
Node counters: ResetClientTraffic tried the node once and dropped a
failure ("nothing replays a reset"); BulkResetTraffic,
ResetAllClientTraffics and ClientService.ResetAllTraffics never told the
node at all. The node kept its pre-reset usage, switched the client off
again on its next tick, and the master latched that - a client shown at
zero usage stayed disabled.
Every reset path now queues a node_pending_resets row per hosting node in
its own transaction. It is delivered right after commit (per-client
endpoint up to the push threshold, bulkResetTraffic above) and replayed by
the node sync ahead of its snapshot, and dropped only once the node
accepted it. While one is owed, the merge takes only that client's usage
from the node, not its enable or limits. Deliveries to a node are
serialized so a reset is not sent twice.
Sidecar quota: BulkResetTraffic, ClientService.ResetAllTraffics and
auto-renew zeroed the panel counters but not mtg's own quota counter, so
the sidecar kept refusing a renewed or reset MTProto client. They now
reset it too, scoped to the affected clients.
Invariant: a client whose stats row is switched off is served by no local
runtime of any inbound it is attached to. client_traffics is email-keyed,
and AddClientStat re-points its inbound_id at the last inbound attached,
yet the runtime push builder and the MTProto, TUIC and AmneziaWG desired-
instance builders looked the row up by inbound_id. On every other inbound
of a multi-inbound client the depletion filter saw nothing, so a depleted
client stayed served there whenever its settings entry still read enabled
- the state the stale settings writes left in existing databases.
All four now resolve the flag through trafficDisabledEmails, keyed by the
emails the inbound actually lists. GetXrayConfig already backfills sibling
rows by email (backfillClientStats) and is unchanged.
Invariant: saving an inbound's configuration never changes a client's
enable, expiry, quota or renewal state, nor the inbound's own traffic
counters. The inbound modal posts back the whole settings.clients list it
loaded when it opened, and UpdateInbound stored it, synced it into the
client records and wrote it into client_traffics. Any client renewed,
depleted or reset while the modal was open was reverted - a renewed client
came back disabled with its old expiry. The row itself was read before the
serialized tx and saved whole, so traffic the poll added in between was
rolled back and a depleted inbound could be re-enabled.
UpdateInbound now re-reads the row inside the writer and, for clients the
inbound already holds, keeps enable, expiryTime, totalGB, reset, resetDay,
resetWeekday and resetMax from it; those change through the client
endpoints. A master's node-sync push stays authoritative. Existing clients'
lifecycle fields are no longer editable through the inbound JSON editor or
/inbounds/update, which the API docs now state.
The README screenshots still showed the v3.2.5 layout. Retake every panel
page in light and dark on the current UI, and redo the annotated Telegram
bot setup shot for the new sidebar layout, marking the enable toggle too.
Invariant: a traffic reset leaves a quota-disabled client enabled
everywhere. ResetTrafficByEmail and BulkResetTraffic enabled the client
first and zeroed its counters afterwards. A traffic tick landing between
the two still saw the client depleted and switched it off again in
client_traffics, the record and settings. Update's direct record write
then set the record back to enabled and the reset zeroed the counters,
leaving the settings entry disabled: the client showed enabled with zero
usage but was dropped from the runtime. The periodic reset job goes through
ResetTrafficByEmail for every depleted client on its cycle, and a node push
inside Update widens the window to seconds.
Zero first, then enable: with the counters at zero the depletion predicate
no longer matches, so the tick has nothing to undo. UpdateInboundClient
re-adds the enabled user to the runtime itself.
Invariant: an operation on an inbound's clients writes back only what it
changed, onto the settings as committed when it writes. Every client op
(add, edit, delete, bulk adjust/detach/delete/set-enable) read the inbound
outside the serial traffic writer and then tx.Save'd the whole row inside
it. A traffic tick that committed in between - auto-renew re-enabling a
client, the delayed-start conversion, a node adoption - was overwritten
with the stale copy. A renewed neighbour ended up enable=false with its old
expiry in settings while client_traffics said enabled, so the next runtime
rebuild dropped a healthy client nobody had touched. The bulk ops then ran
a full SyncInbound from those stale settings, copying enable=false into the
client record too.
Each site now commits through commitInboundClientSettings: inside the
serialized tx it three-way merges the op's edit (read -> output, per client
and per field) onto the committed settings and updates only the settings
column, which also stops the stale up/down/enable inbound columns being
written back. advancePushedInbound now records what the per-client push
delivered rather than the merged settings, so the node's reconcile-skip
fingerprint cannot claim a renewal it never received.
Panels up to v2.x stored a client's tgId as a string, "" when unset. The
startup migration heals copies already in the database, but an exported
inbound imported into a current panel goes straight to AddInbound, whose
typed client parse failed with "cannot unmarshal string into Go struct
field .0.tgId of type int64", so every such import was refused.
AddInbound now runs the legacy normalizer the clients-table seeder already
used (moved from database to model so both share it) over the incoming
settings before parsing them. String numbers become integers and empty ones
are dropped, and the settings are stored in that shape. The same applies to
/inbounds/add callers still sending the old types.
The seeder change is a pure move; the PostgreSQL lane was not run (no
Docker on this host) and is left to CI.
Closes#6663
check_status ran `systemctl status x-ui` and grepped its Active line. That
command also prints the unit's latest journal lines, so it reads the journal
every time the menu is drawn, before most actions, and on hosts with months
of logs the script stalled for a long time before showing its options.
`systemctl show --property=SubState` returns the same "running" state from
the unit alone. The prefix is stripped by hand rather than with --value so it
still works on systemd older than 230.
No test harness covers x-ui.sh. To demonstrate on a host with a large journal:
time systemctl status x-ui >/dev/null
time systemctl show --property=SubState x-ui
Refs #6629
xray's WireGuard inbound credits a packet to the first peer whose allowedIPs
contain its source address, and routes replies by the same table. The panel
only rejected an allowedIPs entry that was string-equal to another client's,
so a pre-assigned address typed in interface notation (10.10.2.9/24, as other
WireGuard tools export it) was accepted and claimed the whole /24: other
clients' traffic and online IPs were credited to that one email, and replies
went to a peer with no endpoint ("no known endpoint for peer").
The collision check now compares masked ranges on every path that uses it:
add, edit, the cross-inbound recheck inside the write transaction, and
AmneziaWG. Auto-allocation skips any address inside a prefix another client
holds. A /0 default route still claims nothing, as before, because legacy
migrated peers carry one. Clients already saved with overlapping ranges keep
working as they do today until edited. The API docs describing the error are
updated, including the stale claim that cross-inbound duplicates are accepted.
Closes#6623
The bot's "individual links" and QR actions fetched the client's own
subscription over HTTP from the public URL it builds for display. With no
sub or web domain set that URL falls back to the machine's hostname, which
usually does not resolve (`lookup exhausted-reply: no such host`), and even
a resolvable name fails behind NAT, a firewall or a disabled sub server.
Both actions now ask InboundService.GetSubLinks, the in-process provider the
panel's links API already uses, with the host taken from that same URL so the
link addresses are unchanged. The now-unused pooled HTTP client goes too.
Closes#6597
The REALITY Min Client Ver hint promised that an empty field accepts every
client version. That is only true on Xray-core v26.9.8+, but the panel's
core switcher still installs v26.7.11 to v26.9.7, where an empty field falls
back to a built-in 26.3.27 minimum and silently diverts older and
third-party clients (Mihomo, sing-box) to the REALITY target. Operators on
those cores were told the field was not the cause.
The hint now names the version boundary in all 13 locales, matching the
docs site. Text-only: no test can reach it; the change is visible in the
inbound form's REALITY tooltip after `npm run build`.
Closes#6568
sponsors.json only had an end date, so a booked placement had to be
added to the file on the day it started. An optional `from` now hides
a sponsor (and its logo) until that instant; entries without it show
immediately as before.
resolvingBind.ParseEndpoint resolved a hostname and then built a
StdNetEndpoint itself. That matches StdNetBind, the default bind on
Linux, but on Windows the default is WinRingBind, whose Send refuses any
endpoint it did not parse ("endpoint type does not correspond with bind
type"). Every handshake initiation failed there, so no AmneziaWG tunnel,
inbound or outbound, could come up on the Windows builds. ParseEndpoint
now hands the resolved literal to the wrapped bind's own parser, which
returns the endpoint type that bind sends to; StdNetBind and pinnedBind
build the same StdNetEndpoint as before.
The resolvingBind tests now read endpoints through the Endpoint
interface instead of asserting StdNetEndpoint, which had pinned the bug.
The panel's SOCKS5 egress for AmneziaWG outbounds bound the fixed
127.0.0.1:64900, and every generated socks bridge dialed that constant.
64900 sits inside Windows' dynamic port range, where the OS can reserve
whole blocks (this host excludes 64885-64984), so on the Windows builds
release.yml ships the listener could stay down and every AmneziaWG
outbound with it. Listen now tries 64900 first and falls back to any
free loopback port; bridges, the outbound probe and the port-conflict
check use EgressPort(), the port actually held. Bridges are generated
apart from the listener, so BuildSocksBridge records the port it wrote
and the AmneziaWG job requests an Xray restart while the listener holds
a different one. Where 64900 is free nothing changes.
The job's restart request is two lines of wiring no test reaches; the
staleness it acts on is pinned by
TestBridgesStaleUntilRegeneratedForTheBoundPort.
Seven tabs, several holding one or two settings, made the page hard to
scan. The encrypted-links switch moves above the tabs under
auto-detection, the colour profile joins the banners under Appearance &
Theme, and Android per-app proxy joins Network & TUN Engine. The QR
modal's settings link no longer needs a happTab selector, and the three
orphaned tab-label keys are dropped from every locale.
The renewal block took a full-width column, pushing Traffic Reset onto
its own line. Each now gets a half-width column like the other fields,
with its follow-up inputs stacked under it.