Commit Graph

238 Commits

Author SHA1 Message Date
MHSanaei 0466e8b0bd fix(finalmask): refuse a salamander password xray-core cannot open
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.
2026-10-11 18:26:45 +02:00
MHSanaei 6321297f68 fix(finalmask): keep a single udphop mask last in every client finalmask
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.
2026-10-11 18:02:12 +02:00
MHSanaei a8255944bf style: trim MASQUE and XDRIVE comment blocks to two lines
The MASQUE and XDRIVE commits left four comment blocks over the
two-line cap CLAUDE.md sets for committed Go/TS.
2026-10-11 17:00:52 +02:00
MHSanaei c7d89ceac2 feat(outbounds): TLS for XDRIVE outbounds
xray-core 26.10.10 dials an XDRIVE outbound's storage API with the
stream's tlsSettings: their SNI, uTLS fingerprint and ALPN are what the
API sees, which is the point of fronting it. canEnableTls kept XDRIVE out
of the TLS-capable networks, so the security picker never offered TLS and
switching a TLS outbound to XDRIVE silently reset it to none.

canEnableOutboundTls admits a TLS-eligible protocol on xdrive and backs
both the picker and the network-change cascade. Inbounds keep the plain
predicate: their tlsSettings carry server certificates, not a client hello.

Also restores the plain oxfmt / oxlint --fix lint-staged commands, reverting
da6872a3: that hook change was not wanted.
2026-10-11 16:05:35 +02:00
MHSanaei ef4bda4e06 feat(inbounds): TUN adapter type and auto-system options
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.
2026-10-11 15:40:46 +02:00
MHSanaei b6c859f5b9 feat(xray): edit Lua DNS and routing scripts
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.
2026-10-11 15:37:20 +02:00
MHSanaei 1c050f872a feat(finalmask): header-custom variables, sudoku UDP mask and noise reset ranges
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.
2026-10-11 15:27:19 +02:00
MHSanaei ec0a57fc4f fix(finalmask): hop client UDP through the udphop mask
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.
2026-10-11 11:20:34 +02:00
MHSanaei 04cccef311 feat(warp): register WARP over MASQUE from the WARP modal
xray-core's masque transport reaches Cloudflare WARP with an enrolled
ECDSA P-256 key, the endpoint's public key and the assigned tunnel
addresses, which only a WARP enrollment hands out.

A new regMasque action on /panel/api/xray/warp/:action registers a
separate WARP device and enrolls a secp256r1 key for the masque tunnel
(the API revision and client version the WARP apps enroll MASQUE keys
with). Enrolling replaces a device's WireGuard key, so it never touches
the stored WireGuard registration. It returns the PEM key pair, the
addresses and the endpoint, and the WARP modal turns them into a
warp-masque outbound (HTTP/3, Cloudflare's MASQUE SNI), or refreshes
the one already there.
2026-10-11 10:31:01 +02:00
MHSanaei b04039c2bd feat(xray): add the MASQUE protocol and transport
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.
2026-10-11 10:28:49 +02:00
MHSanaei ca5983595b feat(xray): add the XDRIVE transport
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.
2026-10-11 08:21:08 +02:00
MHSanaei 73a028f52d feat(xray): update xray-core to v26.10.10 and adapt panel
Move the three Xray binary pins (DockerInit.sh, both release.yml
fetches) to v26.10.10, in lockstep with the xtls/xray-core module that
82c77111 already bumped to 701af60772cd.

xdns finalmask (#7090): a domain now takes a "names" list instead of a
single "name", and a resolver is an "addrs" list of udp:// or tcp://
URLs instead of {type, settings:{addr}}. The loader ignores the old
keys rather than failing, so every mask the 26.9.30 seeder wrote would
load with no domain and no resolver and the tunnel would carry nothing.
maskcompat.UpgradeLegacyXdns now lifts both the pre-26.9.30 string lists
and the 26.9.30 objects straight to names/addrs, dropping entries the
old loader refused. A new seeder (XdnsFinalmaskNamesAddrsFix) reruns it
over every stored finalmask, and the save path, GetXrayConfig, both
link importers and the mask editor's mount lift go through it. Template
outbounds now get the build-time heal too: a 26.9.30 template pasted
after the seeder passes the save check, since the core loads that shape
without error. The editor edits names and addrs as tag lists and no
longer requires record types, which the core now defaults (TXT on a
client, A/CNAME/TXT/AAAA on a server). The wire format did not change,
but clients on an older core cannot read the new config shape.

Lua scripts (#6823): dns.script, routing.script and dns servers[].id
are new. The xray page parsed the dns block with a strict schema, so
loading it dropped script and id and the next save deleted them; the
schema now keeps both and the DNS server modal carries the id through
an edit. The hot routing apply built routing in-process, and that build
now resolves the script file from the panel's working directory, so a
script beside the xray binary failed every rule change into a full
restart and broke the node bridge. RoutingService cannot change the
script anyway, so the panel no longer resolves it there.

TLS client roots (#7105): the core now verifies against its bundled
Mozilla roots instead of the OS store unless tlsSettings.useSystemCA is
set. The outbound TLS form gets a switch for it, so an outbound to a
server signed by a privately installed CA can opt back in.
2026-10-11 07:55:12 +02:00
Leslie Alexander 66234315e4 fix(panel): default TLS ALPN to http/1.1 for WebSocket transport (#6787)
* fix(panel): default TLS ALPN to http/1.1 for WebSocket transport

WebSocket bootstraps over HTTP/1.1: leaving ALPN at the schema default
['h2','http/1.1'] makes the server negotiate h2 and the WS handshake
fails. See issue #6782.

* style(frontend): keep comments within the two-line limit

* style(frontend): keep comments within the two-line limit
2026-10-11 07:17:05 +02:00
Jack 881f646d2c fix(clients): balance renewal form layout and mode guidance (#6672)
* fix(clients): balance renewal form layout and mode guidance

Move renewal out of the traffic-reset half-column so its help and preview
no longer leave a tall empty area beside the reset selector. Keep the
mode, active schedule field and renewal limit in equal desktop columns,
stack them on small screens, and preserve the bulk form's stacked layout.
Keep monthly traffic-reset controls together in their own row and allow
the first-cutoff button to wrap without shrinking its click target.

Show fixed-duration guidance only for interval renewal and the month-end
boundary note only for monthly renewal. Synchronize all thirteen locales
and the client docs, remove obsolete zero-value mode-switching tooltips,
and cover the help transitions through the real client form.

Keep preview markup and existing monthly copy intact so the independent
date-display work in #6662 can merge without overlapping these edits.

This is presentation-only: saved values, expiry selection, renewal and
traffic-reset calculations, APIs and database formats are unchanged.

* refactor(clients): drop the renewal CSS that #6662 made redundant

#6662 landed the full-width preview Space and the wrapping first-cutoff
Button as inline styles, so ClientRenewalFields.css and its wrapper div
styled the same two elements a second time; both are removed.

* test(tuic): wait for downstream association activity refresh

The UDP lifecycle regression can receive its QUIC response before the
server's sending goroutine updates the association timestamp. Reading
the protected field once then mistakes a valid scheduling order for a
missing refresh, intermittently failing the race-enabled CI job.

Wait for lastActive to advance under the registry mutex, bounded by a
one-second deadline, before checking idle reaping. Retain the payload,
association ID, idle-survival and shutdown assertions so a missing
activity update still fails the test.

This only changes test synchronization. Production relay behavior,
renewal logic, network timeouts and CI configuration remain unchanged.

---------

Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-10-08 21:59:38 +02:00
Jack 54d1712544 fix(clients): clarify renewal preview times and estimates (#6662)
* fix(clients): clarify renewal preview times and estimates

The preview rendered RFC3339 strings directly and labeled the server's Local
zone without explaining which clock it represented. Its renewal count also
looked like a record of renewals already performed.

Display readable dates with an explicit UTC offset while preserving the
server's wall time, per-date DST offset and fractional cutoff precision.
Explain the panel/server timezone and read-only preview, and label the
renewal count as estimated allowance consumption in all 13 locales.
Let the first-cutoff button wrap on narrow screens and update the English
and Chinese client documentation plus display/form regression coverage.

This changes presentation only: renewal calculations, stored expiry values,
API payloads and first-use behavior remain unchanged.

* test(clients): remove redundant renewal preview copy checks

The locale-only preview case repeated the Local timezone branch without
covering a distinct behavior. Exact textContent comparisons after matching
the same text with getByText also only pinned constant wording, contrary
to the repository's testing guidance.

Remove that duplicate case, its locale setup and the redundant copy
assertions. Keep the UTC, per-boundary DST offset, fractional precision
and original datetime checks, together with the existing real form-save
tests. Production rendering, translations and renewal behavior are unchanged.

---------

Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
2026-10-07 10:55:06 +02:00
Egor aacfaebab8 fix(tuic): client speed display and certificate button layout (#6723)
* fix(tuic): restore client speed and certificate layout

* docs(tuic): clarify native runtime and protocol behavior

* fix(websocket): preserve traffic updates from independent sources

* fix(docs): sync websocket traffic schema and drop restating TUIC tests

docs/public/openapi.json still described the old traffic event, without
clientTrafficSource/clientTrafficIntervalMs or the TUIC oneOf branch, so
the docs site showed a payload the panel no longer sends. No check
covers that copy.

The TUIC certificate layout test and the TUIC speed payload test only
read back the literals the code writes, so neither could fail on a real
regression. Both are removed, along with the className that existed only
for the layout test.

---------

Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-10-05 19:46:46 +02:00
MHSanaei d7da64f2f0 fix(qr): hide the QR only for links carrying post-quantum keys
A share link's QR is suppressed only when it carries a post-quantum key
payload too large to scan: an ML-DSA-65 verify key (pqv) or an ML-KEM-768
VLESS-encryption auth key. isPostQuantumLink substring-matched "mlkem768"
anywhere in the URL, so it misfired on:

- every VLESS/Trojan REALITY link, since ce221c33 added the
  support-x25519mlkem768=true hint (235 chars, QR version 10);
- every VLESS-encryption link authenticated by an X25519 key, whose
  value always starts with mlkem768x25519plus (321 chars, version 11);
- any remark or host containing mlkem768 / mldsa65 / ML-KEM-768.

All four QR surfaces (inbound QR, client QR, client info, public sub
page) lost their QR button for those links. The detector now reads the
query: a non-empty pqv, or an encryption whose auth key
vlessEncryptionAuthKind classifies as ML-KEM-768.

Closes #6730
2026-10-05 16:30:14 +02:00
Younes Beriane d4a7086c4e fix(inbounds): list clients in the detach/attach modals when they mount open (#6736)
* fix(inbounds): list clients in the detach modal when it mounts open

DetachClientsModal seeded its synced-source state with the open source,
so a modal first mounted with open and a source saw no change on its
first render and never read the client rows. The table stayed empty
until the modal was closed and reopened.

Seed the state with null so the first open render loads the rows, as
the reset-during-render check already expects. The test mounts the
modal already open and fails without this change.

Fixes #6733

* fix(inbounds): list clients in the attach modal when it mounts open

AttachClientsModal seeds its synced-source state the same way
DetachClientsModal did, so a modal first mounted with open and a source
saw no change on its first render and never read the client rows. The
table stayed empty, and nothing was pre-selected, until the modal was
closed and reopened.

Seed the state with null here too. The test mounts the attach modal
already open and fails without this change.

Refs #6733

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-10-05 15:27:56 +02:00
MHSanaei d1b60799ec fix(frontend): show toasts through message.useMessage, never the static API
Invariant: an antd toast lives inside the React tree of the component that
raised it, so it takes the theme from ConfigProvider and unmounts with it.

Four components called antd's static message.* (Happ settings, TUIC form,
routing tab, command palette); the other 40 use message.useMessage. A static
toast renders in a detached root that RTL's cleanup never unmounts, so its
rAF-driven close timer kept updating state for ~3s after a test file ended.
happ-routing-editor.test.tsx logged 419 act() warnings, 226 of them after
its last test; on a slow runner the worker closed first and vitest failed
the job with "Closing rpc while onUserConsoleLog was pending" (CI runs
37075337071, 37303631843). In the product the same toasts ignored the
dark/ultra theme.

The command palette keeps its holder mounted while closed: restartXray
closes the palette before its success toast. The Happ editor test now
asserts its toast unmounts with the component (red on the static API), and
no-static-message.test.ts fails on any static call in src. The spy that
silenced message.success in happ-settings-presets.test.tsx intercepted
nothing after the change and is gone.
2026-10-05 14:01:47 +02:00
Egor 0054e671f8 feat(tuic): implement native in-process Go TUIC v5 server (#6577)
* 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>
2026-10-03 02:56:02 +02:00
Farhan Zare 93847dd106 fix(sub): keep the spider settings in a reality spiderX seed's query (#6694)
* 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.
2026-10-03 00:31:52 +02:00
Artem K 721de5adde Fix fragment exports for older Xray clients (#6702)
* 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>
2026-10-02 17:38:44 +02:00
libmur-dev ce221c33d0 fix(sub): carry REALITY ML-KEM hint in VLESS links (#6712)
* 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>
2026-10-02 15:44:41 +02:00
Matt Van Horn 921ecb0f66 fix: recover panel navigation after stale chunk failures (#6679)
Fixes #6673

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-10-02 15:01:21 +02:00
MHSanaei 99bc68fa14 chore(deps): update project dependencies
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.
2026-10-02 14:23:05 +02:00
MHSanaei 62423cacd1 feat(xray): update xray-core to v26.9.30 and adapt panel
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.
2026-10-02 13:54:39 +02:00
MHSanaei 99047c0a63 fix(frontend): keep the given file name on mobile downloads
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.
2026-09-30 15:03:21 +02:00
MHSanaei aee45ca3fe fix(hosts): advertise Hosts in every WireGuard, AmneziaWG and TUIC config
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".
2026-09-30 15:03:16 +02:00
MHSanaei 823db05966 fix(inbounds): keep the stored client list and enable on inbound save
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.
2026-09-28 13:23:48 +02:00
MHSanaei 8f47b53879 style(settings): fold the Happ settings into four tabs
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.
2026-09-27 16:06:42 +02:00
mrchatam 6f40a75909 feat(inbound): excludeFromSub hides links without disabling (#6463)
* feat(inbound): excludeFromSub hides links without disabling

Add a per-inbound flag that omits subscription output while keeping the
inbound enabled for Xray, auth, and traffic accounting. Fixes #6435.

* fix(inbound): excludeFromSub review follow-ups

gofumpt model.go, sync docs OpenAPI, keep excludeFromSub master-authored
on node mirror, and exercise the legacy add-column migration path in tests.

* fix(sub): keep excluded inbounds' clients in the usage header

The excludeFromSub filter sat in getInboundsBySubId's SQL, so an excluded
inbound's clients never reached seenEmails in the raw, Clash or JSON
renderer. A client that lives only on a hidden inbound (one client per
inbound sharing a subId) dropped out of the Subscription-Userinfo usage,
quota and expiry and out of the info-node state, while the inbound kept
serving it and counting its traffic.

The query returns every enabled inbound again; each renderer skips an
excluded inbound's links but still counts its clients, the same rule the
Clash renderer already applies to external links it cannot express.

---------

Co-authored-by: mrchatam <mrchatam@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-09-27 12:26:37 +02:00
DIMFLIX 71e38367c1 feat(sub): add Incy app-management parameters (#6650)
* feat(sub): add Incy app-management parameters

The panel already pushes a set of Happ headers, but INCY documents its own
lowercase header names and its own value domains, so a Happ-shaped payload gets
ignored by the client (per-app mode is bypass|proxy, not on|bypass, and
per-app-proxy-enable has no Happ counterpart at all). Add a sibling Incy path
that emits exactly the documented headers.

Covered, per https://docs.incy.cc/en/app-management/:
- profile-description, sort-order, support-email, announce-url, premium-url
- banner text/button/URL and the two hex colours
- hide-url, hide-check, no-limit-enabled
- per-app split tunnelling (enable/mode/list)
- TCP fragmentation (enable/length/interval/packets)
- UDP noise packets (enable/type/packet/delay)
- DoH pre-resolution (enable/domain/IP)

Each string setting is tri-state: an empty value omits the header, so an
untouched panel never overrides the subscriber's own choice in the app. Values
are validated against the documented domains and dropped when they do not
match, and non-ASCII text is base64-wrapped the way the docs require for
Cyrillic. INCY identifies itself as INCY/<version>/<platform>, which gates the
headers behind the same auto-detect switch the Happ path uses.

Headers the panel already emits for every client (Profile-Title, Support-Url,
Profile-Web-Page-Url, Announce, Profile-Update-Interval, Subscription-Userinfo)
and Incy's routing line are left as they are.

The Premium API (theme, defaultPingProtocol, fallbackHosts, ...) is a separate
encrypted endpoint and stays out of scope here.

* fix(sub): keep Incy per-app list entries separate on the wire

The Incy settings textarea takes one package per line, as Incy documents for
per-app-proxy-list, but the header path ran the value through
sanitizeHeaderValue, which deletes CR/LF. "com.google.chrome\norg.telegram.messenger"
reached the client as the single bogus package
"com.google.chromeorg.telegram.messenger", so per-app split tunnelling silently
matched no app. Join comma- or line-separated entries as CSV instead.

Also drop three tests that could not fail: TestIncyExcludesHappOnlyHeaders
(ApplyIncyHeaders has no path that emits Happ headers, and the non-Happ UA
gate is already pinned by TestApplyHappHeaders_Gating) and two UI tests that
only asserted updateSetting received the key the JSX passes it.

---------

Co-authored-by: DIMFLIX <dimflix@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-09-27 01:06:17 +02:00
Jack 9672249edb feat(clients): add calendar weekly renewal and schedule previews (#6524)
* feat(clients): add calendar weekly renewal and schedule previews

Expose fixed-day, calendar-weekly, calendar-monthly, and disabled renewal
through one shared selector in individual and bulk client forms. Store the
weekly weekday separately (Monday 1 through Sunday 7) and use panel-local
calendar dates rather than a fixed 168-hour duration. Resolve skipped or
repeated midnights to the first valid instant of the selected date, and skip
an entirely nonexistent calendar date rather than changing the weekday.

Reuse the existing renewal writer and share its boundary alignment and
per-period catch-up calculation with an authenticated, read-only preview.
Keep monthly precedence for legacy records, fixed-day interval semantics,
maximum renewal allowances, first-use durations, and operator-disabled
settings unchanged. Selecting a mode does not rewrite an existing cutoff;
an unset calendar cutoff requires an explicit action to choose the first.
The last-valid-second preview uses the stored exclusive expiry, even when
the billing calculation aligns a legacy last-second cutoff up to midnight.

Carry weekly schedules through client persistence, paging, enable toggles,
inbound settings, and node traffic reconciliation. Migrate missing or nullable
weekday columns to disabled by default without altering existing limits, and
include the new isolated-schema PostgreSQL regression in the live CI gate.

Regenerate API contracts and reference documentation, add lifecycle and form
regressions, and document timezone, quota-reset, and upgrade considerations.
All participating nodes must be upgraded before weekly mode is enabled;
older binaries ignore the new field. Independent periodic traffic resets and
the optional month-end subscription-header display are not changed.

* fix(clients): validate renewal schedules across inbound write paths

Reject conflicting weekly/interval/monthly schedules and out-of-range
weekdays on inbound creation and edits, legacy one-client apply paths,
record/link synchronization, and traffic metadata writes. Validate imported
traffic snapshots as well, before any inbound or client is persisted, so
an inbound API cannot create a client that the clients page cannot toggle.

Merge a weekly-related schedule as one timestamp-selected tuple rather
than filling its zero fields from another renewal mode. Preserve empty
migration snapshots and the existing non-weekly monthly/interval merge
semantics. Renewal caps, counters, credentials, and deadlines are unchanged.

Add regressions for nine write paths, unchanged records and runtime calls
after rejection, valid inbound clients remaining editable, and duplicate
record merges between individually valid renewal modes.

* docs(clients): clarify depleted-client deletion risks on downgrade

Explain in English and Chinese that older versions not only stop weekly
renewal: their depleted-client cleanup can delete a weekly-only client once
its expiry or quota is exhausted. This is conditional on cleanup, not an
automatic deletion caused by downgrade itself.

Recommend backing up and converting weekly schedules to a mode supported
by every participating version before rollback, and avoiding cleanup while
mixed versions or unconverted clients remain. Merely disabling weekly
renewal does not restore the old binary's missing purge protection.

* fix(clients): bound weekly renewal date searches

Limit the search for a valid weekly calendar date to eight candidates so
an unusual timezone cannot monopolize the single traffic writer. Exhaustion
returns the original instant, allowing the existing catch-up forward-progress
guard to stop without advancing expiry, consuming an allowance, resetting
traffic, or falling back to a fixed-duration schedule that can drift.

Reject a non-future calendar suggestion in the read-only preview instead of
offering an immediately expired initial cutoff. Also report failed weekly
catch-up as a search error when allowances remain, not as cap exhaustion.
Existing preview errors use the form's current warning; no API schema or
locale changes are needed.

Exercise exhaustion with a synthetic valid TZif containing twelve skipped
Sundays. This fault-injection case was red without the bound; it is not a
claim that a production IANA timezone was observed hanging. Keep the Havana
and Apia regressions for real skipped/repeated midnights and absent dates.

* fix(tests): isolate weekly renewal preview timezone

Stop the weekly search regression from replacing process-global time.Local.
CI caught that assignment and its cleanup racing with background timer reads
through time.Now, even though the top-level tests do not use t.Parallel.

Pass the timezone and current instant into the unchanged preview calculation.
The public service still validates the request and resolves the panel timezone;
API responses, renewal accounting, and persisted client data are unchanged.

Use fixed dates for both suggestion and catch-up exhaustion, removing the
test's dependency on today's date and its unnecessary database setup. Keep a
bounded-lifetime background clock reader to expose future global-timezone
mutations under the existing race gate rather than disabling that check.

* ci: retrigger PR checks

Create an empty commit to request a fresh pull-request CI run after release dependency downloads failed with network errors.

No source, dependency, or workflow changes are included. Retry the existing checks without bypassing them.

* ci: retry PR checks and record deferred download hardening

Request another CI run after the amd64 release job compiled successfully but failed during dependency fetching with exit code 4 (network failure).

Record possible follow-up improvements for the Linux release fetch helper:
- Print each download URL and destination, and preserve error details.
- Reuse the existing curl configuration with up to five retries; add connection and per-attempt timeouts and a bounded retry window.
- Download to a temporary file and promote it to the final filename only after a successful, non-empty transfer. Keep the job failing if downloads ultimately fail.
- Validate successful downloads, recovery after a temporary failure, and correct failure after persistent errors before shipping such a change.

These improvements are intentionally deferred, not implemented or tested by this commit. This commit is empty: renewal logic, dependencies, workflow configuration, check requirements, and TLS verification remain unchanged.

---------

Co-authored-by: JacktheRanger <219502738+JacktheRanger@users.noreply.github.com>
2026-09-26 22:59:23 +02:00
NgaiYeanCoi 5e15120cec feat(happ): add routing editor, optional ad blocking, and LAN bypass preset (#6545)
* feat(happ): make ad blocking optional in routing presets

Add an independent AdBlock toggle for Iran, China, and global presets, applied only when generating routing rules. Update the China preset to Bypass-CN and cover preset behavior and localized controls.

* feat: add visual routing editor with JSON support and localization updates

- Implemented a new modal for editing routing profiles with basic and advanced tabs.
- Added functionality to load, parse, and generate routing profiles in JSON format.
- Enhanced user experience with validation and error handling for JSON input.
- Updated translations for Russian, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional) to include new routing editor terms.
- Created helper functions for managing routing profiles and generating deep links.
- Added unit tests for routing editor functionalities and JSON handling.

* feat: update routing editor to preserve null lists in profiles and enhance validation messages
2026-09-26 22:57:01 +02:00
Kirill Rudenko 8979072bd9 fix(amneziawg): bound S1-S3 by the receive buffer, reject overlapping H (#6642)
* fix(amneziawg): bound S1-S3 by the receive buffer, reject overlapping H

The native AmneziaWG validator, both Zod schemas, both forms and the docs now
follow the rules amneziawg-go actually enforces.

S1-S3. A padded handshake message is 148+S1, 92+S2 or 64+S3 bytes
(device/send.go). The peer reads each datagram into a [MaxMessageSize]byte
buffer, where MaxMessageSize = MaxSegmentSize (device/pools.go,
constants.go). MaxSegmentSize is 65535 on Linux/Android, 2016 on Windows and
1700 on iOS (device/queueconstants_*.go). The limits are therefore
S1 <= 1552, S2 <= 1608 and S3 <= 1636. Before, S1/S2 allowed 65535, which
iOS peers silently drop, and S3 was capped at 64, a number inherited from the
coinman-dev/3ax-ui port in #6105 with no stated reason. That cap blocked real
configs such as Amnezia Premium's S3=1045. RandomTrailers only tops a packet
up to 500 bytes (DefaultUdpWindow), so it never pushes a message past these
limits.

H1-H4. amneziawg-go refuses the whole device when the header ranges overlap
("headers must not overlap", device/uapi.go mergeWithDevice), and so does the
kernel module (src/netlink.c). The panel did not check this, so an inbound
with overlapping ranges saved and then failed to apply. A blank H is never
sent, so the engine keeps its default, WireGuard's own type 1-4; the check
treats blank fields that way. The docs said 1-4 "must not be used". They are
valid and are the engine default, only unobfuscated without a
HeaderProtectionKey. The docs also said amneziawg-go rejects S1+56 == S2. It
does not (IpcSet accepts it). The panel keeps that rule as a fingerprint
guard, and the docs now say so.

Tests: the new params_test cases and the Zod bounds fail on the old code.
TestValidatedObfuscationAlwaysApplies runs every accepted set through a real
amneziawg-go IpcSet and now covers overlap, blank-H defaults, H=1-4, the
exact S bounds and the full Amnezia Premium set. Before this fix it failed
with "headers must not overlap".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(amneziawg): bound only inbound padding by the iOS receive buffer

The 1700-byte iOS buffer limits what an inbound's clients can receive,
but ValidateObfuscation also runs for outbounds, and the Xray template
save re-validates every AmneziaWG outbound. An outbound whose remote
server uses S1 above 1552 would have blocked every Xray settings save,
though its values come from that server and are received on Linux.

ValidateObfuscation keeps amneziawg-go's uint16 UAPI width for S1-S3;
ValidateServerObfuscation adds the receive-buffer bounds and is what
inbounds call. The outbound schema and form follow the same split.

---------

Co-authored-by: Kirill Rudenko <rudenko@npp-energy.ru>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-09-26 22:02:49 +02:00
SakikoTogawa 5d41e65a3c fix(sub): preserve external VLESS encryption in Clash subscriptions (#6576)
* fix(sub): preserve external VLESS encryption in Clash subscriptions

Copy non-empty, non-none encryption from parsed external VLESS settings,
matching local proxy export. This prevents merged Clash/Mihomo subscriptions
from losing the encryption parameters of externally added nodes.

Cover encryption normalization and omission, plus merged YAML from pasted
links and HTTPS subscriptions containing plain or Base64 share-link lists.

Validation: regression cases fail before the fix and pass after it; the full
subscription package and go build ./... pass. Four unrelated packages still
fail on Windows, with the same failures reproduced using the original code.

Refs: MHSanaei/3x-ui#6572

* test(nodes): wait for chart effects in history panel assertions

The DOM can commit its accessible labels before Sparkline updates the refs used by uPlot range callbacks. Wait for the existing assertions together so the test does not read the empty-data range.

Reproduced the original CI failure locally on attempt 5. The fixed test passed 12 consecutive runs; lint, format and TypeScript checks pass. The full frontend suite passed 1607 of 1608 tests, including Storybook. The unrelated input-number guard fails on Windows because execFileSync cannot launch the extensionless oxlint shim (ENOENT); invoking oxlint.cmd reports all three expected diagnostics.
2026-09-26 20:31:50 +02:00
MHSanaei fd7b3559bc feat(panel): add sponsor slots fed from sponsors.sanaei.dev
Monthly sponsor placements need to change without cutting a panel
release. Panels now read 3X/sponsors.json from the MHSanaei/sponsors
repo (GitHub Pages on sponsors.sanaei.dev) and show active sponsors in
four slots: an overview banner, a rotating sidebar card (max three), the
login page and a new Sponsors page that also lists open placements.

An entry shows only while enable is not false and until is in the
future; links must be https and logos are png/webp/jpg by name only.
The list is cached for an hour and the last good copy survives upstream
failures; logos are proxied through /sponsors/logo/:name with failures
cached, so CSP stays 'self' and admin browsers never reach a third
party. Admins can hide a slot for 24h. Under XUI_DEBUG the panel reads
a sibling ../sponsors/3X checkout so edits can be previewed before push.
2026-09-26 03:31:51 +02:00
MHSanaei 89e200ead4 fix(frontend): key geo entries by page position and clear test-suite noise
Zod 4: use the `error` param instead of the deprecated `message`.
lint:deprecated missed these because tsgolint's no-deprecated does not
resolve object-literal properties on a `string | Params` union.

Geodata: key geo entry rows by page position. antd deprecates rowKey's
index argument, and kind:value repeats within a page because the reader
drops domain attributes (22 pairs in geosite_IR.dat, 108 in geosite_RU).

Nord/PIA: the "All cities/regions" option used a null value, which antd
warns on. Map it through a sentinel at the Select boundary so form state
stays null, with tests that fail when the sentinel is not mapped back.

Tests:
- Run the oxlint guard through node; .bin/oxlint is a sh shim Windows
  cannot spawn, and the swallowed error left both guard cases vacuous.
- Start unit workers with --no-experimental-webstorage; msw's localStorage
  probe made Node 25+ warn once per forked worker.
- Set IS_REACT_ACT_ENVIRONMENT, which RTL never sets with globals: false,
  and settle the async updates it exposed inside act(). The row-cells
  memo test now fails when memo is removed.
- Disable antd's click wave in Storybook; it re-rendered inside the next
  story's act() and tripped "not configured to support act".
- Assert InboundFormModal's validation log instead of leaking it, and
  give the rule-form test a well-formed clients/list response.
2026-09-25 21:21:03 +02:00
Sanaei 17e89db979 feat(hosts): add a cipher suites override and accept custom suites
The inbound TLS form offered cipherSuites as a closed single-choice list,
but xray reads the value as a colon-separated list and accepts any name Go
knows, so several suites or one missing from the list could not be set.
Both the inbound and the new host field now use a tag picker that keeps
the stored value as the colon-joined string xray expects; old single
values open unchanged.

A host's cipher suites replace the inbound's in the JSON subscription
stream, and a blank field inherits them. Share links and Clash carry no
cipher suite parameter, so their output is unchanged.
2026-09-16 11:56:20 +02:00
Sanaei b78dd82869 fix(nodes): chart node net throughput in KB/s, not percent
The node history panel passed its Net Up / Net Down series to Sparkline
without valueMax or yFormatter, so they inherited the percentage defaults:
a fixed 0-100 scale and a "%" label. Any node above 100 KB/s drew off the
top of the chart and every axis tick and tooltip read as a percentage.

A Sparkline fed non-percentage data has to declare its own scale and unit;
every other call site already did, only the two node net series did not.
2026-09-16 11:48:55 +02:00
Sanaei e26cf1d3ed feat(sub): redesign the subscription page around usage, tabs and app imports
The info page was a long key/value table followed by every link and two
app dropdowns, and it rendered left-to-right even for Persian and Arabic.
It now leads with a usage ring, the remaining quota and a stats grid, and
splits the rest into Subscription / Apps / Configs tabs.

- Status tells expired, data-used-up and disabled apart instead of one
  "Inactive", replacing the hard-coded English expiry chip.
- The Apps tab keeps every Android and iOS app with its existing deep
  link, preselects the visitor's platform and adds Windows: Hiddify and
  Clash Verge Rev import directly, v2rayN copies the link.
- fa-IR and ar-EG render right-to-left; URLs, IDs and sizes stay LTR.
- The footer shows the support link and the client refresh interval, so
  subPageContext now carries subUpdates (also in ?format=info).
- Status, days-left and app deep-link logic lives in subPageModel.ts,
  with unit tests pinning the deep links the page already shipped.
2026-09-16 01:55:56 +02:00
Sanaei e8bab17c2f fix(clients): stop the Edit Client modal showing a stray light scrollbar
The client form body is capped at the viewport and scrolls internally
(49ef1449). Every tab ends with a Form.Item that keeps antd's 24px bottom
margin, so when the fields themselves fit, that empty margin alone pushed
the body past the cap: 752px of content in 740px at a 900px window. The
last item of each tab now drops the margin, so the body scrolls only when
real content overflows.

When it does scroll, the bar was painted light inside the dark modal: the
dark themes set body.dark and data-theme but never color-scheme, which is
what native scrollbars read. applyDom (panel, login and subscription
bundles) and the Storybook decorator now set it on the root element.
2026-09-15 22:04:00 +02:00
Sanaei 14b92fbcff fix(nodes): stop flagging a node on the other update channel as outdated
A node's "update available" tag compares its reported panel version with the
master's latest, and any non-semver side fell back to string inequality. A
dev build reports dev+<sha> (config.GetPanelVersion), so a node moved to the
dev channel from a master on the stable channel kept the tag forever; the
reverse, a stable node under a master on the dev channel, was flagged too and
the tag's default stable update installed nothing new.

A dev label and a release tag carry no order, so the comparison now only
decides within one channel; dev-to-dev still compares commits, which keeps a
node on the current dev-latest commit untagged as config.go intends.
2026-09-15 21:21:36 +02:00
NgaiYeanCoi 1d85ef138e fix(sub): prevent default profile page URL disclosure (#6538)
* fix(sub): prevent default profile page URL disclosure

Add explicit none, builtin, and custom profile page modes.
Preserve existing custom URLs and warn before exposing the built-in page.
Cover mode selection, legacy settings, and subscription response headers.

* fix(subscription): add profile page link options and upgrade notes
2026-09-15 21:13:29 +02:00
Sanaei 3fa44915c1 perf(nodes): keep the node table element across unrelated re-renders
rc-table re-runs every cell renderer whenever the Table re-renders, and
NodeList rebuilt its columns and table props on every render (the relative
time formatter was a fresh function each time). Any re-render of the Nodes
page therefore re-rendered all rows even when no node had changed: about
390ms per re-render for 150 nodes in jsdom.

The formatter is now stable and the table element is memoized on its
inputs, so a re-render that leaves the nodes untouched costs 0.5ms. A
heartbeat push that does change the nodes still re-renders every row.
2026-09-15 21:06:32 +02:00
Sanaei 7fc86f87de perf(inbounds): keep unchanged rows and online sets across websocket pushes
Every client_stats push carries the totals of all inbounds, and
applyClientStatsEvent rebuilt each row it listed, so every push replaced
all rows, re-ran the client rollup (a JSON parse of every inbound's
settings) and re-rendered the whole table even when no number moved. Every
traffic push also built new online and active maps, re-running the same
rollup.

Rows are now rebuilt only when their totals or a client's numbers change,
and the previous maps are kept when a push repeats the same sets. Measured
in jsdom with 450 inbounds of 50 clients each: an unchanged client_stats
push went from 7.9ms to 0.6ms with no row rebuilt, and a repeated traffic
push from 13.5ms to 8.3ms without the rollup.
2026-09-15 21:06:32 +02:00
Sanaei bc424f0968 fix(xray): stop a lone dns qType 0 from matching every query
The core reads a dns rule's qType as a PortList, which drops a bare numeric
0 (infra/conf/common.go: `if number != 0`), and a rule with no qTypes
matches every query. A stored `"qType": 0` therefore does not target query
type 0: it drops, refuses or hijacks all DNS through that outbound.

A qType the panel writes has to be read by the core as exactly the query
types it names. Four writers broke that:

- DNSOutboundLegacyKeysFix rewrote a lone blockTypes [0] into "qType": 0,
  so "block type 0" became "block everything" on upgrade.
- That seeder shipped in v3.8.0 and is recorded as done, so fixing it does
  not reach installs that already ran it. DNSOutboundQTypeZeroFix spells
  any stored numeric qType 0 as "0" once, protocol id matched like the core.
- The outbound form adapter turned a typed "0" into the number 0.
- The Xray template editor saves raw JSON past that adapter; the save now
  applies the same rewrite.

Each writer is pinned by a test that fails without its part. The rewrite
and the repair compare policies as the pinned core builds them, and the
repair runs through runSeeders over a database whose legacy-keys seeder
already ran, on SQLite and PostgreSQL 16.
2026-09-15 16:00:05 +02:00
BlindMaster24 c0271e231d fix(panel): read the outbound protocol id in the Outbounds row like the core (#6528)
* fix(panel): read the outbound protocol id in the address column like the core

outboundAddresses switched on the raw id, so a row the core runs normally but
spelled "VMess", "Trojan" or "WireGuard" fell through to default and rendered
an empty Address column in the outbounds table, the card view and the
subscription table -- a populated server that looks absent, which is what
sends an operator to recreate a correct outbound.

The id is folded once before the switch, the way isUdpOutbound already folds
the transport name.

* fix(panel): fill the outbound address column from one protocol-id rule

outboundAddresses folded the id inline while isUntestable, two functions
below it in the same file, reads it through isOutboundProtocol — so the
"the core lowercases the id" rule lived in two places and two tests. It
now routes through the shared helper, which keeps the rule with the
module that owns it.

Two further gaps in the same switch, reported in the same review:
hysteria and amneziawg are both selectable in the outbound form but had
no case, so a canonically spelled row rendered a blank Address cell that
case folding could not reach; and the VLESS branch returned a bare ":"
for a row whose servers sit in vnext, which this change newly reached
for a "VLESS" spelling.

Tests: the hysteria/amneziawg cases and the bare-separator case are red
on the pre-fix switch.

* fix(panel): read the vnext shape of a vless outbound in the address column

The vless branch read only the flat settings.address/port, so a row whose
servers sit in vnext — the shape the probe's extractor reads first
(internal/web/service/outbound/outbound.go:259-269) — rendered a bare ":"
separator, or nothing at all before this branch folded the id. It now
reads vnext first and falls back to the flat pair, the order the
extractor uses, which also makes it agree with what a probe of that row
would say.

Test: "reads the vnext server of a vless row" is red on the pre-fix
branch.

* fix(panel): read the protocol id of the outbound stream tags like the core

The identity cell gated the network and security tags on an exact-match
includes() over four ids, so the same "VMess" row whose address this
branch now shows still rendered without its ws/tls tags — the row was
half-readable. It now asks the shared isOutboundProtocol, the rule every
other reader on the page uses.

Test: "renders the stream tags and the address of a VMess row" is red
without this change (['VMess'] vs ['VMess','ws','tls']).
2026-09-14 19:53:57 +03:00
BlindMaster24 efcf152950 fix(outbound): read the probe testability gate's ids like the core (#6527)
A direct, DNS, loopback or blackhole outbound is not a proxy, so the probe
must reject it instead of measuring the panel host's own reachability. The
gate compared the protocol id exactly while the core lowercases it in
LoadWithID before resolving the handler, so "Freedom" and "DNS" were not
recognised: the HTTP probe ran through the direct outbound and returned
Success=true with a full egress block, and the row's Test button stayed
enabled because isUntestable compared exactly as well. The operator reads the
panel host's own country and delay as a working tunnel.

The batch gate now folds the id once before its switch, and isUntestable goes
through the shared isOutboundProtocol helper.
2026-09-14 19:53:05 +03:00
BlindMaster24 84c5aef4a1 fix(panel): probe UDP outbounds and hide the block outbound from the mtproto egress picker (#6525)
* fix(panel): match the probe and egress readers to what the core loads

Two readers left over from the case-sensitivity sweep still disagreed with
the core, both raised reviewing #6523.

isUdpOutbound compared the protocol id and the transport name exactly. The
core lowercases both before it resolves them (infra/conf/loader.go:46 for
the id, TransportProtocol.Build at infra/conf/transport_internet.go:16-17
for the name), so an outbound spelled "WireGuard" or a stream named "KCP"
still built a UDP handler but was probed with a dial-only TCP request, and
Test All Outbounds reported a working outbound as down.

The mtproto egress picker asked for outbound tags without excludeBlackhole,
so the block outbound stayed selectable there. Choosing it looks like a
working selection and discards that inbound's Telegram traffic.

* fix(panel): recognise the mkcp transport alias and pin the picker's field id

Review findings on #6525.

TransportProtocol.Build resolves both "kcp" and "mkcp" to the same mKCP
transport, so comparing the transport name against "kcp" alone left a
template spelling "network": "mkcp" in the TCP lane and reported a working
outbound as down — the same trigger this PR already fixed for the "KCP"
capitalisation.

The egress picker now carries an explicit id, the way the inbound form's
protocol select does, so the test addresses that field rather than the first
searchable select on the page and reuses the shared dropdown helper instead
of duplicating it.
2026-09-14 16:00:13 +02:00