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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
joinHostPort wrote an endpoint address into the authority of every URL
share link unencoded, and genVlessLink alone skipped encodeUserinfo for
the client id. Both can come from a node's sync (an externalProxy dest,
an adopted host row, the client id), the finished link is never
re-parsed, and the raw subscription is one link per line: a line break
in either added lines of the node's choosing to the master's
subscription, and an id carrying "@" moved the client's VLESS links on
other inbounds to another server.
Whitespace, control characters and URI delimiters in the host are now
percent-encoded, which leaves host names, IP literals and zone ids
unchanged, and the VLESS id is encoded as userinfo like every other
protocol's credential.
gregorianToJalali walked the calendar one month at a time from a 2024
reference, so an expiry near the int64 limit (a node reports expiries
unchecked) cost seconds of CPU on every subscription fetch that renders
{{JALALI_EXPIRE_DATE}}. Jalali leap years repeat every 33 years, so
whole 33-year blocks are skipped before the walk.
Remote routing sources are fetched documents the panel re-renders on
every subscription request. A Clash YAML document stays small through
aliases, which decode to one shared value but are written out in full,
so it now has to fit a 4 MiB written-size budget and 16 levels of
nesting when it is accepted (and when a persisted copy is loaded). A JSON
routing profile could repeat route groups, each copy repeating every
site list; a group now counts once, since a repeat can never match.
The YAML encoder writes multi-line strings as block scalars and gets
CR and the Unicode line breaks (NEL, LS, PS) wrong inside them, so a
remark, path or header name with such characters could continue as YAML
outside its scalar. Strings with a non-printable rune or invalid UTF-8
are now written double-quoted with escapes, map keys included.
A client's external subscription is fetched from a third party on every
subscriber refresh, and the fetcher trusted it more than its siblings:
- it dialled any address, redirects included; it now goes through the
SSRF guard like the outbound subscription and remote routing fetchers;
- it forwarded any line containing "://"; it now keeps only server share
links, so app deeplinks and plain URLs never reach the subscriber's app;
- it bounded the body but not the link count (now 1000) or the headers
(now 64 KiB), and stored the raw error, URL included, on every row; the
stored error is now host-only and at most 1 KiB.
The link parser also refuses links that nest JSON past 16 levels, since
the JSON subscription pretty-prints it at quadratic cost, and external
configs are marshalled compactly until the final, single indent pass.
The node certificate probe builds a transport for one request and dropped
it with the connection still parked, and the remote routing client kept
idle connections with no timeout; either stayed open for as long as the
remote side wanted. Close the probe's idle connections when it returns,
and turn keep-alive off for routing downloads, which are minutes apart.
The routing client dials only public hosts, so no loopback test reaches
it; TestFetchCertFingerprintLeavesNoIdleConnection covers the probe.
Xray builds the ClientHello with uTLS for every fingerprint except
"unsafe", and GetFingerprint("") resolves to chrome, so cipherSuites are
dropped unless the fingerprint is exactly "unsafe". A host or inbound
that set cs and left the fingerprint blank shipped links whose cs did
nothing in v2rayN, PattN and every other Xray-based client.
Raw links (query and vmess JSON), the JSON subscription and the panel's
own link generator now emit fp=unsafe when cs is present and the final
fingerprint is empty. An explicit fingerprint is left as configured.
Every subscription fetch read and scanned each inbound's whole settings
blob, about 25 MB at 100k clients, although subscription bodies resolve
their clients from the clients table and never use its clients array.
Each inbound's settings minus the clients array are now cached, keyed by
inbound and shared by all its subscribers; the inbound row, clients,
stats and hosts are still read per fetch, and WireGuard and AmneziaWG keep
full settings for their peers. Panel edits purge the cache through an
invalidator registry the subscription server releases on stop, with a
10 s TTL as the backstop. At 100k clients a hit takes 2.1 ms on SQLite
(72.5 ms before) and 20.7 ms on PostgreSQL (332 ms).
Rotating SNIs meant one host entry per domain. A host now takes a pool of
SNIs: with more than one, every generated config gets a random entry,
or, with the shared switch on, the whole host group uses one pick per
subscription fetch. Raw, JSON and Clash output, the panel's link preview
and node adoption all carry the pool.
sni stays the pool's first entry, so a rolled-back binary and API callers
that only know the single field keep working; an sni outside the pool
becomes its primary. A host with one SNI renders exactly as before.
Deleting the only host a client was bound to blanked that client on every
inbound. Once the host row was gone, the binding could no longer say which
inbound it guarded, so it failed closed everywhere: a client on inbounds A
and B, bound to one host on A, lost B's link the moment that host was
deleted, and nothing in the panel hinted at it.
Every host delete (host groups, the externalProxy sync, inbound deletion,
the depleted-inbound sweep and the orphan prune) now goes through
database.DeleteHosts, which keeps each deleted host's inbound in
host_tombstones in the same transaction. A binding to a deleted host closes
only that inbound; ids that resolve to no host at all still fail closed
everywhere. The Postgres Host step now runs the deletion cases as well.
Host rules applied per inbound only, so every client of an inbound got the same
subscription endpoints (#6618). A client can now be bound to one or more host
rules, and its raw, JSON and Clash subscriptions render only those hosts, in
either subscription order; an unbound client is unchanged. A binding whose hosts
are deleted, disabled or excluded from the format fails closed: the client gets
no links there but stays in traffic accounting. Host edits keep row ids, so
bindings survive them; nil leaves a stored binding alone and an empty list
clears it.
The hosts-first order (#6895) rendered each host through its own endpoint
builder, which skipped the CIDR expansion from #6784, so a range host went out
as the bare network address. One helper, hostEndpointMaps, now builds a host's
entries for both orders.
Hosts already had one saved order on the Hosts page, but subscriptions honoured
it only inside each inbound, so a host of a later inbound could never come before
one of an earlier inbound (#6894). A new opt-in setting makes raw/base64,
Clash/Mihomo and JSON subscriptions follow the Hosts page order across inbounds;
the default keeps today's inbound-first output unchanged. The Hosts table gains
drag-and-drop reordering next to the Up/Down buttons, on a pointer-reorder hook
the Routing Rules table now shares.
A shadowsocks inbound with a TCP HTTP header was dropped from Mihomo
subscriptions, although Mihomo's simple-obfs speaks the same wire format
(#6735). Such a node is now emitted with plugin: obfs, mode http and the request
Host header. Mihomo always requests GET /, and xray-core rejects a request whose
path is not one of the header's paths, so only headers that accept / map;
others stay dropped, as do non-shadowsocks protocols and the legacy Clash
endpoint.
Hosts and TLS inbounds can set cipher suites, but they reached only the JSON and
Clash subscriptions; raw share links never carried them, so clients that read cs
from links (v2rayNG forks used with fp=unsafe) had to enter them by hand. Share
links now emit cs from the host or inbound TLS settings, the panel's own link
generator and previews do the same, and both link parsers (the Go one and the
panel's outbound import) read cs back into tlsSettings.cipherSuites.
AmneziaWG clients accept a randomised PersistentKeepalive range such as 15-25,
which makes keepalive timing harder to fingerprint; the panel only stored a
single number. A client's keepalive may now be a range, validated the way
AmneziaWG parses it. The downloadable .conf and vpn:// profile carry the range,
Clash/Mihomo output gets its lower bound, and plain WireGuard stays scalar-only.
The range lives in an additive wg_keep_alive_range column, so the integer
column and an older panel binary keep working after a rollback.
The raw, JSON and Clash bodies carry live traffic, expiry and routing headers
but went out without Cache-Control, so any cache in front of the sub server
could serve stale data, such as old routing rules after an admin edit. The rule
lived in scattered per-branch calls that new branches missed. setNoCacheHeaders
now runs as middleware on the sub, JSON, Clash, Mihomo and legacy Clash route
groups, covering every current and future response of those per-subscriber
routes.
The per-host Mihomo X25519 switch has been stored since managed hosts landed,
but no renderer read it, while every REALITY proxy was forced to
support-x25519mlkem768: true. On some networks DPI drops the second segment of
an ML-KEM ClientHello and blackholes the server for that client. A host with the
switch on now gets support-x25519mlkem768: false and no ML-KEM hint in its vless
link; switches saved while they had no effect are cleared once, so upgrading
does not silently change existing subscriptions.
A REALITY host's fingerprint now reaches the Clash and JSON outputs, and every
host endpoint gets its own deep copy of the stream, so one host's blank SNI,
fingerprint, Host header or path no longer inherits another host's value.
Operators who front a server with an IP range (a CDN or anycast prefix) had to
list each address as its own host. A host address may now be a CIDR, and every
subscription fetch expands it into up to randomCount (1–100, default 1) unique
random host IPs from the range, IPv4 and IPv6 alike, skipping the network and
broadcast addresses. The panel's own previews, QR codes and tunnel config
exports (WireGuard, AmneziaWG, TUIC, MTProto) expand it the same way, and the
externalProxy importer writes the model's default count so the re-sync still
recognises untouched imported hosts.
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.
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.
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.
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.
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
* 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>
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.
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.
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.
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.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.
xray-core 26.9.9 dropped finalmask.quicParams.udpHop for the client-only
"udphop" UDP mask {mode, interval, remoteIPs, remotePorts}. The panel kept
writing the old key, so hysteria/xhttp outbounds and JSON-subscription
clients silently stopped hopping ports.
An inbound keeps quicParams.udpHop: it is panel metadata advertised as the
link's mport, and a udphop mask on a listener fails with "udphop: client
only". The conversion (maskcompat.UpgradeLegacyUDPHop, intervalRemote mode)
runs only where a client config is produced:
- UDPHopClientMaskFix seeder over hosts, sub-JSON finalmask, cached
outbound-subscription outbounds and template outbounds (never inbounds);
- GetXrayConfig heals template outbounds at build time;
- the JSON subscription upgrades each generated client stream;
- the Go and frontend link importers upgrade a legacy fm= param.
The finalmask editor gains a side prop: the client side lifts the dead key
on mount and offers a udphop mask editor (also for xhttp's HTTP/3 dial);
the inbound side keeps the UDP Hop switch, labelled as advertised.
xray-core v26.10.10 serves MASQUE: CONNECT-IP over HTTP/3 or HTTP/2,
always behind TLS, as an inbound and an outbound protocol riding a
dedicated "masque" transport. The panel could not create either.
Inbound: MASQUE is a multi-user protocol whose users log in with their
email and password over Basic auth. The panel keeps the client's
"password" like trojan and GenXrayInboundConfig hands it to the core as
"pass", so the full-config path, the live AddInbound path and node
pushes all emit what the core reads; one user with an empty pass makes
the core refuse the whole inbound. Live user adds build a masque.Account
from either key, client-only changes diff per user so a new client does
not drop every connected tunnel, and client creation, copy, validation
and edits key on the password. The address pool and MTU get form
fields, TLS is mandatory, and the port check follows the core's
listener choice: HTTP/2 on TCP when the ALPN offers h2, HTTP/3 on UDP
when it offers h3 or not h2. Nodes may host MASQUE from v3.9.1, the
first panel release that knows to rename the password.
MASQUE has no share-link format, so links and Clash skip it; the JSON
subscription builds the client's masque outbound with its email and
password, and the subscription allowlist now lets a subId see MASQUE
inbounds at all. The IP-limit job, client pages and traffic tracking
list the protocol.
Outbound: the form edits the server, remote DNS, and the transport's
host, path, Basic credentials, headers and WARP block (enrolled key,
endpoint public key, tunnel addresses), which xray-core refuses next to
user/pass. The outbound test probes it through the core, since its
default HTTP/3 dial is UDP.
xray-core v26.10.10 tunnels a stream through files in shared cloud
storage ("network": "xdrive"): Google Drive, a local folder, or any HTTP
storage API described by a template. The panel could not select it, and
the stream schema rejected a pasted xdrive stream outright.
The inbound and outbound forms now offer XDRIVE with one shared editor:
service, folder, the three Google Drive secrets in the order the core
reads them, a template JSON editor with its {secretN} values, the
optional domain front (the stream-level address/port the storage client
dials), and the core's tuning knobs with their defaults as placeholders.
Switching an inbound to XDRIVE resets security to none, because the
core would otherwise read the inbound's TLS/REALITY as the storage API's
client TLS.
XDRIVE has no share-link format, and a link carrying the storage
secrets would print them into every QR code, so both link generators
emit none for it; the JSON subscription already passes the stream
through intact, and Clash drops the unknown transport.
The JSON subscription flattens tlsSettings into xray's client shape before
the per-host loop, then applyExternalProxyTLSToStream writes the host's
echConfigList, verifyPeerCertByName, pinnedPeerCertSha256 and
allowInsecure into the panel-only tlsSettings.settings map. Xray ignores
that map, so a host's ECH (and its pins / verify name) never reached the
client and connections through the host failed.
After the host overrides are applied, lift that map through the same
writer tlsData uses for the inbound's own TLS: fields land at the top of
tlsSettings, pins are joined into the comma string xray parses, and
allowInsecure is dropped exactly as it is for the inbound (removed from
xray). The Clash renderer still reads the nested map and is unchanged.
The helper-level subtest that pinned the nested location as the "json"
shape is replaced by an end-to-end GetJson test on a host-backed inbound.
Closes#6743
* fix(sub): append host serverDescription to hysteria links
A host's Description reached vless/trojan/ss through buildEndpointLinks, but
genHysteriaLink renders the fragment in its own externalProxy loop and never
added the suffix, so Happ fell back to its "Hysteria | hysteria | TLS" caption
for every Hysteria server on a host that also serves VLESS (#6738).
Reuse appendHappServerDescription with the description the endpoint map already
carries, so no key lookup is duplicated and a host with no description emits the
same bytes as before. genTuicLink (service.go:912) has the same gap; left alone
to keep this diff to the reported protocol.
Regression test is red without the fix for both hysteria:// and hysteria2://.
* chore(sub): trim the hysteria serverDescription regression test
The no-description test passed with and without the #6738 fix: the empty
description branch is already pinned by TestAppendHappServerDescription, so
it certified nothing about this change. Also cut the remaining test's comment
block to the two-line limit CLAUDE.md sets.
---------
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
* 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>
* 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.