Integrations could only find a client by subscription ID through a substring
search over the paged client list followed by a second lookup by email.
GET /panel/api/clients/get/subId/:subId now returns the full client records
(settings, attached inbounds, external links, traffic, tunnel addresses) for an
exact subId match, as a list because older rows may share an ID. It sits behind
the same admin login or admin-token check as the email lookup.
A panel or API request rejected for a missing or wrong CSRF token got a bare
403 and left nothing in the log, so an operator chasing failed logins from a
stale tab or a script had no trace. CSRFMiddleware now logs a warning with the
method, path and client address. The address comes from getRemoteIp, which
honours the trusted-proxy list, instead of gin's ClientIP, which trusts any
peer's X-Forwarded-For.
Inbound XMUX counts such as maxConnections are stored as JSON numbers, but
buildXhttpClashOpts kept only string values, so the panel default
maxConnections: 3 never reached Clash/Mihomo clients. Numeric values are now
rendered as plain integers (FormatFloat, so 1000000 does not become 1e+06) and
zero values are still dropped.
One lost heartbeat on a lossy link flipped a node offline and sent node.down,
and the next probe sent node.up, so a flaky node flooded Telegram, email and
Discord with alert pairs.
A new nodeDownThreshold setting (default 1, today's behaviour) works like
outboundDownThreshold: node.down fires once the failed-check streak reaches it,
once per outage, and node.up only after a node.down was sent. The announcement
is persisted in nodes.down_notified, so a restart inside the threshold window
neither drops the outage's node.down nor re-announces nodes already reported
offline; nodes offline at upgrade are seeded as announced.
pages.index.download labels the download-speed figure on the dashboard next to
"Загрузка" (upload). The imperative "Скачать" read as a button; the noun
"Скачивание" matches what the label shows. CI does not run on translation-only
changes; the file still parses as JSON.
Running out of quota switches a client off in client_traffics, in its client
record and in the inbound settings JSON. The inbound's periodic reset and the
Reset All Client Traffic button only set client_traffics.enable back, so the
client showed zero usage but stayed out of the running core every period.
Both paths now read the disabled, depleted emails inside the transaction that
zeroes the counters, on the traffic writer, and re-enable them through
BulkSetEnable after the commit, like every other reset path. Reading them inside
that transaction keeps a traffic tick from disabling a client between the read
and the reset.
Public Mihomo templates ship rules, rule-providers and proxy-groups together
with a dns block those rules depend on (fake-ip ranges, nameserver policy).
Pasting a template inline keeps its dns, but pointing the global routing rules
at its URL dropped it, so URL users had to give up auto-refresh to keep it.
A new switch, subClashRemoteDNS (off by default), also takes the remote dns and
sniffer maps. It is opt-in because a remote dns block decides where clients send
every lookup. A malformed dns or sniffer value is skipped, not the whole template.
The component-test teardown now outlasts AntD Form's 10ms error debounce, which
otherwise fired after jsdom was gone.
Subscriptions build an inbound's links from its Host rows, and those rows are
imported from streamSettings.externalProxy only when the inbound is added. An
update that moved the external proxy returned 200, but every subscription kept
handing out the old address, port and security.
An update now re-syncs only the externalProxy entries it changed onto hosts that
are still exactly as imported. Unchanged entries are set aside first, so a host
the admin edited, disabled, deleted or shared is never re-imported next to the
admin's version, and a stream without the externalProxy key leaves hosts alone.
getRemoteIp took a trusted proxy's X-Real-IP first, otherwise the leftmost
X-Forwarded-For entry. A proxy that forwards a client-sent X-Real-IP
unchanged (Caddy, HAProxy) or appends to X-Forwarded-For leaves both values
to the client. X-Forwarded-For is now read from the right, skipping trusted
proxies. X-Real-IP is believed only when it agrees with that hop or stands
alone, and a disagreement falls back to the proxy's own address.
The documented Caddyfile and the docs reverse-proxy generator now overwrite
X-Real-IP with the address Caddy saw.
The first user is now created when the server starts, with a random
username and password printed once to its log (docker logs, the journal),
instead of a fixed pair seeded by InitDB. CLI calls that initialize the
database are often piped away, as the Docker entrypoint's port lookup is, so
only the server's own log reliably reaches the operator.
`x-ui setting -show` keeps reporting hasDefaultCredential: true until a user
exists, so install.sh still sets its own credentials on a fresh install.
Existing databases are not changed. Verified on a built binary: fresh
setting -show, install.sh-style credential set, first start, login and
restart. The PostgreSQL lane was not run locally; the change adds no query.
The WireGuard and AmneziaWG .conf builders (subscription service, clients
page, inbound modal and subscription page) checked only a few of the values
they write. Each builder now assembles the file line by line and renders
nothing when a line holds a control character, so every value is covered
whatever stored it. A decoded vpn:// payload is refused when it carries a
wg-quick hook directive, which no panel builder writes, and the subscription
page hides a config block that rendered nothing.
The inbound form now validates a WireGuard/AmneziaWG client's allowedIPs on
create and on update, including the stored clients a protocol switch keeps,
as the client endpoints already did.
An inbound with a total traffic cap is switched off once up + down
reaches it. Its periodic traffic reset then zeroes the counters but
never sets enable back, so after the first used-up period the inbound
stays off for good and the reset schedule does nothing for it.
The reset job now notes, before resetting, whether a local inbound was
off because of its own cap (and not expired), and switches it back on
through SetInboundEnable once the counters are zero. An inbound switched
off below its cap, or past its expiry, stays off.
Deleting a client tombstones its email for 90s, so a node snapshot taken
before the delete reached the node cannot resurrect it. Client create and
bulk create withdrew that tombstone, but a client re-created through
InboundService.AddInbound (SyncInbound / syncInboundClients) did not. On a
master, re-adding a node inbound with a just-deleted email therefore let
the next node merge filter the new client out of the snapshot, unlink it
from its inbound and mark it a sync orphan; ReapSyncOrphans then
hard-deleted it 15 minutes later and the node lost it.
syncInboundClients now records the emails it creates on the serialized
transaction, and runSerializedTx withdraws their tombstones only after the
commit, still on the writer, so no merge runs in between and a rolled-back
create leaves the tombstone standing. The per-call withdraws in Create and
bulkCreate are covered by this and removed.
Co-authored-by: FZ1010 <FZ1010@users.noreply.github.com>
* fix(node-sync): do not latch node disable when master allows (#6870)
nodeDisableIsStale trusted any node-reported enable=false whose limits matched
the master's, treating it as a genuine verdict and latching it on the master.
But matching limits no longer prove the verdict: a single stray false
(a depleted snapshot the master later re-enabled, a stale
disableInvalidClients hit, or a push that lagged a quota reset) leaves the
client stuck disabled on the node, and every master tick flips the client's
state again. This is the loop reported in #6870.
Make nodeDisableIsStale a single rule: stale iff the master's own deadline
and quota still permit the client. A genuine combined-quota or expiry
disable (masterLimitsAllowClient == false) still latches, preserving #4917.
Mirror the same rule in ClientTrafficEnableMergeExpr so the SQL UPDATE and
the Go in-memory replay cannot drift.
* fix(node-sync): keep node usage over quota latching a node disable
Dropping the matching-limits latch (#6870) let the master re-enable a
client the node had genuinely counted past its quota whenever the
master's own counters lagged, e.g. after a seeded-at-zero adoption,
reopening #4917. A node's enable=false now stays latched when the
node's own usage already spends the master's quota; otherwise the
master's limits decide, so a stray disable no longer loops.
Restores TestNodeQuotaDisable_SameExpiryStillLatches as it was on main.
---------
Co-authored-by: Alireza Ahmadi <alireza7@gmail.com>
* fix(frontend): make the panel usable with screen readers
* fix(frontend): keep mobile tab names and the inbound expiry hint
Review of #6907 found two gaps in the screen-reader pass:
- On phone-width screens catTabLabel renders only the icon and names the
tab by putting aria-label on it, but callers now pass that icon with
aria-hidden for the desktop label, so every Settings, Xray and Host
category tab lost its name under TalkBack. catTabLabel now clears
aria-hidden on the icon it uses as the name.
- DateTimePicker forwarded id and aria-label but not aria-describedby,
so the inbound expiry hint ("leave blank to never expire") was never
attached. It now forwards it on both the Gregorian and Jalali input.
Bump xtls/xray-core to 1b1647cd9ddf (v26.10.21) and move the three
binary pins (DockerInit.sh, both release.yml fetches) in lockstep. The
v26.10.10 release was pulled upstream, so the old pins already 404.
XDRIVE (#7182): the core now runs XDRIVE only for VLESS inside VLESS
encryption, with no flow, and an outbound only over Mux; it refuses the
whole config otherwise. The panel offers XDRIVE only for VLESS, refuses
to save an inbound without decryption, never emits a client flow on an
XDRIVE stream (config, JSON subscription, forms), and turns Mux on when
an outbound picks XDRIVE. An inbound or template outbound stored before
the upgrade that the core would refuse is left out of the generated
config with a warning, so it no longer stops every other inbound. The
service "Google Drive" is now "google-drive" (old name read as a
deprecated alias); saves, the schema and both form adapters store the
new name. XDRIVE and MASQUE gain a client xmux; both schemas keep it
instead of stripping it on an inbound save.
Lua scripts (#7124, #7136): the core refuses a routing script beside
rules or domainStrategy, and a dns script beside the fallback/parallel
switches or a domain-scoped server. The panel always keeps its api
routing rule, so a template save now refuses any routing script and a
dns script in a refused combination, and a stored one is dropped from
the generated config so the core still starts. Noise masks gain a Lua
script that the loader compiles inside Build: the in-process build
(template save check, hot add) now resolves a relative script the way
the xray child does, beside the xray binary, instead of failing it. The
mask editor gets a Script field that replaces the noise list.
XDNS (#7140): a resolver takes https:// DoH addresses with worker,
tlsConfig and quicConfig. The mask editor registered only addrs, so a
save dropped the rest; it now edits all four.
Finalmask filter (#7206): a new TCP/UDP mask screens an inbound's source
IPs. The inbound mask editor offers it with its allow and block lists;
it is a no-op on a dialer and an older client core refuses the type, so
the share link and JSON subscription drop it like the Go link already
does.
* feat(auth): add node-admin API token scope
Keep ordinary node-sync credentials unable to self-update a remote panel while allowing explicitly elevated node administrators to inherit synchronization access and invoke updatePanel.
* style(docs): format API scope table
* fix(auth): preserve mTLS node-sync semantics
Allow only an explicit node-admin bearer to elevate a verified mTLS request. Existing admin, monitor, and node-sync bearer values continue to use the historical node-sync identity on mTLS connections.
* chore(ci): rerun flaky checks
---------
Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com>
The server-side fetch used sponsors.sanaei.dev, a hostname unique to
3x-ui. Without ECH it travels in plaintext SNI, so an on-path observer
could tag a server as running the panel from that one connection.
raw.githubusercontent.com is shared by countless tools and reveals
nothing. The path stays inside TLS. The data source is unchanged: the
same MHSanaei/sponsors repo.
Refs #6824
mtg-multi v1.16.0 adds [defense.pending-handshakes] max-per-ip, which
closes new connections from an IP already holding that many
unauthenticated handshakes, so one address cannot pin the workers other
clients need. The MTProto inbound form now carries it as
pendingHandshakesMaxPerIp; the key is written only when set above 0, so
existing inbounds render a byte-identical config and are not restarted.
A change to it moves the structural fingerprint and restarts the sidecar.
dry-run is left out on purpose: it only feeds the pending_handshake_limit
Prometheus metric, which the panel does not surface. The hint warns that
clients behind carrier NAT share the limit.
A vmess link has no extra blob: the sub emitter writes the XHTTP extra
fields (session/seq/uplink placement, padding obfs, xmux, ...) at the
top level of the vmess JSON. The Go importer only read xPaddingBytes,
scMaxEachPostBytes and scMinPostsIntervalMs from there, so a vmess
XHTTP inbound with, for example, sessionIDPlacement=header or
xPaddingObfsMode came back from an outbound subscription or a client
external link as an outbound that cannot connect.
Read the same keys the TS importer already reads from vmess JSON,
including the pre-rename sessionPlacement/sessionKey aliases.
#6480 taught the vless/trojan importers to rebuild the mKCP header and
seed and left the vmess importers ignoring fm. The vmess emitter writes
the finalmask as fm, the mKCP header as type and the seed as path, plus
mtu/tti. Both importers dropped all of them, so a vmess mKCP inbound
with a seed, a header or any finalmask UDP mask (salamander, ...) came
back from an outbound subscription, a client external link or the
outbound form's link import as an outbound that cannot connect.
Feed those fields through the same finalmask and kcp helpers the
url-param protocols use, in the Go and TS importers.
LogWriter inspected each pipe chunk on its own. The Go runtime writes
"panic: " with a separate write before the message and traceback, so the
crash report was written from that first chunk alone and the panel left
7-byte core_crash_*.log files holding only "panic: ". Any ordinary log
line mentioning panic/exception/stack trace was also treated as a crash
and dropped from the panel log.
The writer now splits output into lines, starts a crash only on a line
beginning with the Go runtime's "panic: " / "fatal error: " header, keeps
everything after it, and writes one report from Flush, which
waitForCommand calls after cmd.Wait has delivered all output. The header
line becomes LastLine so GetResult shows the cause.
The report filename format "_000000000" is not a Go fractional-seconds
layout and always produced literal zeros, so reports from the same second
overwrote each other; os.CreateTemp now gives every report a unique name.
A salamander finalmask carrying a packetSize range is Gecko obfs. The
hysteria2:// link generators already export it, but buildHysteriaProxy
copied only the password, so Clash/Mihomo received plain salamander and
could never complete the handshake against a Gecko server.
Emit mihomo's gecko form instead: obfs: gecko plus integer
obfs-min-packet-size / obfs-max-packet-size, validated by the same
parseHysteriaPacketSize bounds the URI export uses. A password-only mask
keeps the plain salamander output.
Closes#6822
The per-user hot path only covered vless, vmess, trojan, hysteria and
masque, so every client change on a multi-user shadowsocks inbound - a
node push for an auto-disable, renew, reset or add, a local Edit Inbound
save, or a config-diff hot apply - fell through to DelInbound +
AddInbound, closing the listener and dropping every live connection on
the port.
Shadowsocks joins userDiffableProtocols. The emitted user ops also carry
the inbound's method as "cipher": 2022 clients have no method of their
own, and without it AddUser cannot build the account, which would leave
2022 inbounds rebuilding anyway. Changes outside settings.clients
(method, 2022 server key, network) still differ in the rest of the
settings and still rebuild.
Verified against the pinned core: TestXrayAPI_E2E_Users and the
shadowsocks e2e tests pass with XRAY_E2E_BINARY set.
Closes#6820
Every 5s node merge rewrote one client_traffics row and one
node_client_traffics baseline per (inbound, client) pair the node
reported, even when nothing had moved: a client is reported once per
inbound it is on, and idle clients every tick. On top of that each
inbound's SyncInbound wrote its own per-inbound values onto the shared
clients record, so a client on a flow and a non-flow inbound (or on a
WireGuard tunnel plus any other inbound) had its record flipped back
and forth twice per merge. With 7 nodes and ~20k client links that is
tens of thousands of statements per tick, all inside transactions on
the single serialized writer, holding row locks for seconds. A renew
of a client on 38 inbounds takes one writer turn per inbound, queued
behind those merges each time, and outlived the 30s HTTP write timeout
even though it committed; last_sub_fetch updates waited on the same
locks.
The merge now computes the row its UPDATE would leave, mirroring the
SQL merge expressions, and skips the write when nothing changes; the
baseline upsert is skipped when the node counter has not moved. The
record's per-inbound fields get one owner each, so repeated syncs
agree: flow is settled to the effective flow (lowest-id link carrying
one) for clients whose links changed, and WireGuard/AmneziaWG peer
fields come from the client's lowest-id tunnel. A repeated unchanged
snapshot now writes no client row; one active client writes two.
Verified beyond make verify: the CI Postgres lane, the merge and flow
tests run against PostgreSQL 16, and make node-e2e.
Closes#6815
The sidebar no longer carries a sponsor card. The backend stops serving
the "sidebar" slot, so a sponsors.json entry that still lists it keeps
only its other slots and one listing nothing else is dropped. Its
rotation, three-sponsor cap and collapsed-rail logo rendering go with it.
The Sponsors page now lists only real sponsors: the "Your brand here"
card shows only as the empty state instead of sitting in the grid and
repeating the header's Become a sponsor button, and the Available
placements section and its slot-occupancy helper are removed. Cards are
larger, two per row on wide screens. Unused i18n keys are dropped from
every locale.
* sub: isolate Incy subscription routing from Happ headers
The resolved Incy line is written to the body only when IsIncyClient
matches the subscription User-Agent, Incy routing is enabled, and the
resolved text is non-blank. ApplyCommonHeaders computes that same Incy
check once and, for a matching client, skips the Happ routing-header
block, leaving Routing-Enable, the Happ Routing value, and the JSON
routing header unset. ApplyIncyHeaders is called with that incyClient
flag. The new test requests the subscription as INCY/1.0.0/ios and as
Happ/1.2.0 and expects the Incy response to end with the incy:// line
and to have empty Routing headers, and the Happ response to keep the
happ:// Routing header with Routing-Enable true and no incy:// line in
the body.
Fixes#6062
* fix(sub): keep the shared JSON routing profile for INCY clients
The INCY isolation wrapped the whole Routing block, JSON fallback
included, in !incyClient. INCY therefore lost the subJsonRoutingRules
profile that #6402 sends as the Routing header so Happ and INCY fetch
the geo files the baked rules reference, on the raw, JSON and Clash
endpoints alike, while every other client kept it, incy:// deeplinks
included.
INCY accepts a routing link of any scheme in that header
(docs.incy.cc/en/routing), so only the Happ tab is Happ-only: an INCY
request now reads it as blank and falls back to the shared profile,
as the setting text promises. Where the raw body carries INCY's own
profile the header is dropped, because INCY ranks a Routing header
above a base64 body line (docs.incy.cc/en/app-management) and the
shared profile would override it, which is #6062 again.
---------
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
A host's verify_peer_cert_by_name must only hold a server name an
operator typed. The column started life as a boolean, and the migration
that was meant to discard those values runs after AutoMigrate, which has
already cast the column to text: SQLite's table rebuild stores 0/1 as
"0"/"1", and Postgres' USING col::text stores "false"/"true". The
migration then sees a text column and leaves the values alone.
Since 3.10.0 the JSON subscription lifts host TLS verification fields to
the level xray reads, so that leftover "0" is now sent as the cert name to
verify and clients such as Happ refuse the server certificate.
A one-shot HostVerifyPeerCertByNameBoolFix seeder blanks exactly the four
cast values on existing databases; real names are untouched. The
Postgres variant joins the CI schema job, since that cast only happens
there.
The core puts a WireGuard outbound on a kernel TUN whenever it holds
CAP_NET_ADMIN and noKernelTun is unset, the default for a hand-added outbound.
That needs a writable /proc/sys/net and /dev/net/tun, and where either is
missing the core does not fall back: it aborts the whole start. The shipped
docker-compose grants NET_ADMIN for fail2ban while Docker keeps /proc/sys
read-only and exposes no /dev/net/tun, so one such outbound took every inbound
down with the #6796 error; any unit or drop-in that keeps /proc/sys/net
read-only did the same.
xray.KernelTunBlocked probes once, from the panel process whose capabilities
and mounts the core inherits, whether the core would pick a kernel TUN it
cannot create. GetXrayConfig then sets noKernelTun on every WireGuard outbound,
so cold starts and hot reloads both get it. Hosts that can create the device
keep the kernel TUN.
Verified with the pinned core's "run -test" on the generated config, in a
NET_ADMIN container as docker-compose runs it and under systemd 257: the
container and the v3.10.0 unit report blocked and pass on gVisor, the container
fails with the #6796 error when the GetXrayConfig hook is bypassed, and the new
unit keeps the kernel TUN.
Refs #6796
A master pushes the whole inbound to its node whenever a client on it is
auto-disabled, renewed or reset, and the node-sync reconcile re-sends every
inbound whose payload changed (all of them after a master restart). The
node's UpdateInbound answered every push with DelInbound + AddInbound, so
Xray closed the listener and every open connection on that port dropped.
On a node inbound with ~2,700 clients that was dozens of times a day.
UpdateInbound now diffs the config Xray is running against the new one.
When only settings.clients differ (vless, vmess, trojan, hysteria, masque)
it applies per-user RemoveUser/AddUser through the runtime, and when
nothing the core sees changed it does nothing. Any other change, or any
per-user failure (a handler the core lost included), still rebuilds as
before. AmneziaWG joins MTProto and TUIC on rt.UpdateInbound, whose
in-place Ensure keeps the device and its peers; Del+Add tore it down.
A pushed removal does not consult the node's own
restartXrayOnClientDisable: it defaults to on, and the master already
restarts its nodes from its own setting (restartRemoteNodesOnDisable).
Nodes on an older release keep rebuilding until they update; the master
side needs no change.
Verified: the new service tests, TestXrayAPI_E2E_Users against a real
core (pins the missing-user vs missing-handler error texts) and make
node-e2e. make race not run: no goroutine, lock or shared state changed.
Closes#6795
* fix(xray): keep balancer overrides across routing reloads
A balancer override set from the panel lived only inside the running
core. Every routing hot-apply (any Xray-settings save touching rules or
balancers) and every node test through an outbound calls
RoutingService.AddRule with shouldAppend=false, which rebuilds all
balancers, so the override vanished silently even though the API docs
promised it lasted until an Xray restart.
The panel now records the overrides it sets, per core process, and
re-applies them after its own routing reloads. Targets are validated
against the running config: the core accepts any tag and then closes
every connection routed to one it lacks, so an unknown target is
refused, and an override whose outbound is removed is cleared instead
of black-holing the balancer. Balancer targets resolve to their _bl_
loopback from the running config rather than the saved template.
Restarting Xray still clears overrides, as documented.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrJrzn9LKaPhMq9h3Fb5TV
* fix(xray): serialize overrides and the node bridge with config applies
Review of #6779 found two races. Neither path took the package lock that
RestartXray holds while it changes the running core:
- An override landing mid hot-apply was checked against the config
snapshot the apply was replacing, so it could pin a balancer to an
outbound the core had just removed. OverrideBalancer now takes the lock.
- The node bridge's teardown checked overrides against the snapshot it
took before the probe, which can run for seconds, so it dropped an
override set meanwhile onto a newly saved outbound.
The same stale snapshot drove the bridge's routing restore. That restore
silently reverted rule changes saved during a probe. After a restart, it
also pushed the old core's rules and overrides into the new core through
the shared API port. The bridge is now built and torn down under the lock,
which is not held while the probe runs. At teardown it restores the
routing that the core's config holds then, and it leaves alone a core that
was restarted meanwhile.
With every caller under the package lock, balancerOverrideSet drops its
own mutex.
VLESS inbounds whose settings carry no "encryption" key (rows saved by
panels before the field existed, or inbounds added over the API with only
"decryption":"none", as the API docs example does) produced a JSON
subscription outbound with "encryption": "". Xray refuses to load that
config ("VLESS users: please add/set \"encryption\":\"none\" for every
user"), so every VLESS entry of the JSON subscription was unusable. The
share link left the parameter out instead.
Treat a missing or empty value as "none" for both outputs, matching the
frontend inbound schema default and the docs link generator.
* fix(amneziawg): respect tunnel MTU when writing UDP replies
Send replies through gVisor FindRoute/Route.WritePacket instead of WriteRawPacket so the network endpoints apply the tunnel MTU and fragment oversized IPv4/IPv6 datagrams. Preserve UDP checksums including the zero-complement IPv6 case and validate lengths before narrowing to uint16.
Regression tests inspect and reassemble actual channel-backed TUN output at and above the MTU, verify checksums and exercise concurrent sources.
* docs(review): put the tally before Design and name who can store a trigger
xray-core opens an XDRIVE stream's storage only when the inbound starts
listening, so a Google Drive inbound saved with the form's defaults (no
folder, no secrets), a local one without a folder, or a template one
whose template xray cannot use stopped xray and every other inbound with
it. validateXDriveStorage applies the core's storage rules at save, on
inbound add/update and on the xray template's inbounds and outbounds:
the service must be local, Google Drive or template; local and Google
Drive need a remoteFolder; Google Drive needs its three non-empty
secrets; a template must pass newTemplateStorage's checks (put, get,
list and delete urls, and a list namesRegex with one capture group).
A VLESS client kept its xtls-rprx-vision flow when its inbound moved to
a transport that cannot carry it, and GetXrayConfig only honoured
DisableFlow, so the server demanded Vision there and refused every
client. GetXrayConfig and the outbound form now apply the Vision gate,
and that gate now follows the core in the backend, the subscription
service and the frontend: TLS/REALITY on raw TCP, or VLESS encryption on
any transport (xray runs Vision inside encryption.CommonConn whatever
the transport, not only on XHTTP). The outbound modal passes the VLESS
encryption into the gate and gates Mux on the effective flow.
chunkStrings/chunkInts sized their result with (len+size-1)/size, so a
size near MaxInt wrapped negative and make panicked with "cap out of
range"; count the chunks with div+remainder instead. NormalizeUDPHop's
len-hops+1 is bounded by len(masks) since hops >= 1, so cap at that.
Resolves CodeQL go/allocation-size-overflow alerts #119 and #120.
emailUsedByOtherInbounds and emailsUsedByOtherInbounds always queried
through database.GetDB(), yet five callers run them inside a transaction:
both node traffic sync sweeps (setRemoteTrafficLocked), the inbound edit
stats sync (updateClientTraffics) and both email-drop branches of
UpdateInboundClient. Each call needed a second pool connection while the
transaction held row locks on client_traffics/node_client_traffics. On
PostgreSQL, once subscription fetches (UPDATE client_traffics SET
last_sub_fetch) blocked on those locks filled the rest of the 25-connection
pool, the lookup waited forever, the transaction never ended, and the panel
hung until restart.
Both helpers now take the *gorm.DB to query on; transactional callers pass
tx and the rest pass the pool handle they already hold. The new tests cap
the pool at one connection, so any query that escapes the transaction
blocks; each went red before the fix on SQLite and (node sync) on a live
PostgreSQL 16, and passes after.
Closes#6789
xray-core builds a Shadowsocks-2022 inbound with users only for the AES
methods: the multi-user identity header is AES-only, so the config
builder and the multi-user server both reject chacha20. Every panel
inbound carries clients, so picking 2022-blake3-chacha20-poly1305 in the
inbound form kept the whole core from starting; its links also failed
on the client with "multi-key is not supported".
The inbound form no longer offers the method and AddInbound/UpdateInbound
reject it. Startup moves stored inbounds to 2022-blake3-aes-256-gcm,
which takes the same 32-byte keys, so every stored key stays valid.
Outbounds keep the method, since a single-key outbound to such a server
works. The share-link generator drops the single-user branch that only
this method reached.
xray-core parses the inbound-level Shadowsocks-2022 key with ParseKey and
refuses one of the wrong size while building the inbound, which keeps the
whole core from starting, not only that inbound. Only client keys were
checked, so an API caller posting a 32-byte key on aes-128 (or keeping a
16-byte key after switching to aes-256/chacha20) saved fine and took
Xray down.
AddInbound and UpdateInbound now reject such a key with a message that
names the method and the size it needs. The check uses the core's own
GetCipherMethod/ParseKey, so a raw non-base64 key the core accepts stays
valid. Startup also replaces a stored wrong-size server key: that inbound
never worked, so no client holds a working copy of it.
The panel scheduler runs WithSeconds(), whose parser rejects a standard
five-field crontab such as "30 8 * * *". An admin who typed one into the
Telegram/Discord notify time or the LDAP sync schedule saved it without
error, AddJob then failed at startup, and the job silently never ran.
Admin-typed schedules now go through internal/util/cronspec, which takes
an optional seconds column plus the @descriptors and hands the scheduler
a parsed cron.Schedule. CheckValid rejects an unparseable schedule on
save, and a stored legacy value falls back to the job's default instead
of dropping the job.
xray-core needs a salamander (and gecko) password of at least 4 bytes
(smPSKMinLen), but checks it only when a socket opens: xray run -test
reports Configuration OK. On an inbound the listener then fails and Xray
does not start at all, taking every other inbound down; on a client the
dial fails. The panel saved any password, and the finalmask editor
starts a new salamander mask with an empty one.
maskcompat.CheckSalamanderPasswords applies the core's byte rule, pinned
against the core's own ListenPacket, and every path that stores a
finalmask refuses a short password: inbound add and update, hosts, the
subscription's global finalmask, and the inbounds and outbounds of the
Xray template. The editor flags it inline as it is typed.
xray-core reverses finalmask.udp and refuses, at dial time, a mask that
dials its own socket (udphop) anywhere but the socket end: "incorrect
index". xray run -test still says Configuration OK, so the failure only
shows as a client that never connects.
UDPHopClientMaskFix turned a global subJsonFinalMask hop and a host hop
into udphop masks; the JSON subscription then appended both, leaving two
hoppers with the first one misplaced, and a host mask merged after a
global hop pushed it off the end too. A hand-ordered template outbound
or an imported fm= could carry the same order.
maskcompat.NormalizeUDPHop keeps the last-listed udphop alone at the end
of udp, and runs wherever a client finalmask is produced: each JSON
subscription endpoint, share-link fm=, the Go and frontend link
importers and the template outbound heal in GetXrayConfig. The JSON
subscription resolves competing hop ranges most specific first: a host
hop wins over the inbound's own, which wins over the global default.
MASQUE logs a user in with the email as the Basic-auth user name, so
xray-core refuses the whole inbound (MASQUE: invalid "email") over one
email holding ":", and with it the Xray start. The panel accepts such an
email for every other protocol, so attaching or creating that client on
a MASQUE inbound took every inbound down on the next restart.
syncInboundClients is the write every create, attach, bulk, import and
inbound-edit path shares inside its transaction, before the runtime
push, so the per-protocol check there now refuses the email for MASQUE
next to the existing TUIC identity check, loading the protocol once.
UpgradeLegacyUDPHop deleted udpHop from the quicParams map it was handed
in place. The JSON subscription runs it once per endpoint on a finalmask
that mergeFinalMask (host masks) or withLegacyFragmentRanges cloned only
shallowly, so the first endpoint emptied the inbound's shared quicParams
and every later endpoint got {"quicParams":{}} and no udphop mask: a
hysteria inbound with port hopping and two hosts hopped on the first
host's config only.
The upgrade now replaces quicParams and udp on the finalmask it is given
instead of writing into values a merged finalmask shares, as the
frontend's upgradeLegacyUdpHop already does.
xray-core 26.9 added three TUN settings the inbound form could not set:
- desc, the tunnel type a new Wintun adapter is created with on Windows.
The settings schema did not list it, so the parse stripped it and a
value entered as JSON vanished on the next save;
- autoSystemDnsToGateway (Linux), which points systemd-resolved at the
TUN gateway;
- autoSystemWfpBlockLeak (Windows), whose dns / misconfigtun filters keep
traffic from leaking past the TUN.
The tooltips name the platform each one acts on and what it needs, since
the core refuses to build or start the TUN otherwise.
xray-core 26.10.10 can hand DNS and routing to Lua: dns.script and
routing.script name a .lua file (absolute, or found next to the Xray
binary) that defines HandleDNSQuery / HandleRoute, and a DNS server's id
is how a DNS script picks it. The keys round-tripped through the JSON
template but had no field.
- DNS: a Lua Script field, and an ID field in the server modal;
- Basics: a Routing Script field beside the routing strategy;
- Routing: a warning above the rules while a script is set, since the
core then routes only through the script and skips every rule, the
panel's api rule included.
The descriptions say the script must still send the api inbound to the
api outbound, and that Xray will not start if the file is missing. An
empty field drops the key, because the core rejects an empty path.
xray-core 26.6 grew the finalmask surface the editor could not express,
and the finalmask watch drops any key without a registered field, so a
config written by hand lost these keys on the next save:
- header-custom items can capture their bytes into a variable, reuse one
or emit a transform expression; a Kind select keeps each item to exactly
one kind, as the core requires, and only parsed JSON reaches transform;
- TCP header-custom gains the errors group (the reply sent when a client
group does not match), UDP header-custom its prefix/standalone mode;
- sudoku is offered as a UDP mask, and share links keep it in fm=;
- the noise reset is an Int32Range, so it now takes "60-120" as well.
The new golden fixture is saved unchanged by the outbound editor and
built by xray-core in TestGoldenStreamFixturesBuildInXray.