Unix Domain Socket inbounds (listen path starting with /) use port 0, which xray-core ignores. Validation was hard-locked to a minimum of 1 in three places: the shared Zod PortSchema, the AntD InputNumber, and the Go Inbound model tag. Adds an InboundPortSchema (min 0) for the inbound form/API schemas, makes the port InputNumber min UDS-aware, and relaxes the Inbound model validate tag to gte=0. PortSchema and the Node model stay min 1.
RegWarp now stores config.client_id from the Cloudflare registration, and WarpModal sources the reserved bytes from the live config response (falling back to stored creds). Previously reservedFor read an always-missing client_id, producing an empty reserved array.
Hysteria links now carry the pinned peer cert under the hysteria2-standard pinSHA256 key instead of pcs (frontend genHysteriaLink + outbound importer round-trip), and the Go subscription generator emits ech from echConfigList. Also drops the dead allowInsecure guard in genHysteriaLink, which read a field that does not exist on TlsClientSettings.
Hysteria doesn't use uTLS, but the outbound TLS form's uTLS dropdown only listed concrete fingerprints (chrome, firefox, ...) with no explicit empty entry. Add a None option, matching the inbound TLS form, so the fingerprint can be left empty.
parseHysteria2Link hardcoded alpn to h3 and never read fp, ech, or the fm (finalmask) param, so importing a Hysteria2 client URL as an outbound dropped the configured ALPN, fingerprint, and salamander UDP mask. Parse alpn (falling back to h3 only when absent), fp, ech, and the pcs pinned-cert key, and restore the UDP mask via applyFinalMaskParam.
xray-core reads the bind-interface sockopt as json:"interface", but the schema and forms used interfaceName. Go's JSON unmarshal is case-insensitive, yet interfacename != interface, so the value never reached xray and interface binding silently did nothing. Rename the field across the schema, the inbound/outbound forms, and the golden fixture to match xray-core and the official docs.
The trafficDiff InputNumber and form schema lacked an upper bound, so values above 100 were accepted in the UI but rejected by the backend (gte=0,lte=100), failing the entire settings save with a misleading 'request body failed validation' error. Add max=100 to the input and .max(100) to the schema.
The link-to-JSON importer dropped two VLESS Reality fields:
- pqv (post-quantum ML-DSA-65 verify key) was never parsed; map it back
to realitySettings.mldsa65Verify, matching the inbound link generator.
- encryption was force-reset to 'none' in the form adapter regardless of
the parsed value, discarding post-quantum encryption strings.
Add regression tests for both paths.
Rename the DNS rule wire key qtype to qType (reading the legacy qtype on parse for back-compat), add the new rCode response-code field for the return action (omitted when zero), and rename the reject action to return. Align the DNS rule action set across the form dropdown, schema, and adapter to the core's valid values (direct/drop/return/hijack), dropping the never-valid rejectIPv4/rejectIPv6 entries.
Consolidate the eight legacy mKCP/header UDP mask types into a single mkcp-legacy type ({header, value}), simplify xicmp to {dgram, ips}, and add the new realm UDP mask type, matching the updated Xray-core wire format. Update the FinalMask schema enum, the transport form, the mKCP seeding default, and the backend KCP share-link translation. Refresh golden fixtures/snapshots and add backend coverage for the mapping.
The routing-rule tag picker reads inboundTags from the xray config query
(['xray','config']), but refresh() only invalidated the inbounds/clients
buckets. So after adding, editing or deleting an inbound the tag list stayed
stale until a hard refresh wiped the react-query cache. Invalidate the xray
config query too, alongside the existing inbounds-options fix.
A non-empty, non-any Address (listen) leaked into the tag as
in-<listen>:<port>-<transport> (e.g. in-127.0.0.1:443-tcp). The tag is
now always in-<port>-<transport>, with the node prefix and numeric dedup
suffix still handling uniqueness across nodes and same-port/different-listen
inbounds. Mirrored in the Go authority and the TS form preview, kept in
parity by tests.
Existing colon-form tags are now treated as custom, so editing such an
inbound preserves its tag rather than rewriting it; new inbounds (or a
cleared tag field) get the clean form.
setImmediate is a Node global not declared in the frontend's DOM tsconfig,
so tsc --noEmit failed with 'Cannot find name setImmediate'. setTimeout is
universally typed and still flushes React's pending setImmediate: looping
the awaits keeps afterEach unresolved across several event-loop iterations,
so the queued check-phase callback fires while window still exists.
React 19 defers passive-effect flushes onto a setImmediate callback that
reads window.event. When one was still queued as vitest tore down the
jsdom environment, it fired after window was deleted and surfaced as an
unhandled 'window is not defined' error, failing the run with exit 1
despite all tests passing. Drain the macrotask queue in afterEach so any
pending callback runs while window still exists.
The Online column already surfaced last-online on the offline tag; extend the same tooltip to the depleted (ended) tag so a depleted client's last activity is visible without enabling it.
The client column under-counted clients attached to an inbound whose shared client_traffics row is keyed to a different inbound: rollupClients filtered settings.clients down to emails that had a stat row on that inbound. Count from settings.clients membership instead. Also surface all/active/disable/depleted/online with the Clients-page color scheme and widen the column.
The summary card derived active/bucket counts from the live client_stats snapshot, which only refreshed on the next traffic broadcast (up to 5s). A removal therefore left the counts stale while only total tracked the refetched server summary. Clear the snapshot in invalidateAll so the card falls back to the authoritative server summary immediately; the next stats event repopulates it for live tracking.
List pages wrapped content in <Spin spinning={!fetched}> where 'fetched' only flipped true once data arrived. With staleTime: Infinity + retry: 1, a transient network error on first load left the query in a permanent error state and the spinner stuck forever.
Now 'fetched' also settles on query.isError, and a failed load shows a Result error card with a Refresh button that self-heals when the backend returns, mirroring the existing XrayPage pattern. Applied to clients, inbounds, groups, nodes, and the dashboard.
Fixes#4723
Adds an 'Attach Existing Clients' row action on multi-user inbounds (shown even when the inbound is empty). It opens a modal listing the whole client pool with search and group filter, all attachable clients pre-selected, and attaches the selection to that inbound via the existing bulkAttach endpoint. Clients already on the inbound are shown disabled and skipped. Translations added for all 13 locales.
Adds the ability to update node panels to the latest release from the Nodes
page: select online, enabled nodes (checkboxes) and trigger their official
self-updater, or use the per-row Update action. A node whose reported panel
version trails the latest GitHub release is flagged with an 'update available'
tag (compared via lib/panel-version, mirroring the Go isNewerVersion).
Backend: Remote.UpdatePanel calls the node's existing
POST /panel/api/server/updatePanel; NodeService.UpdatePanels fans out over the
selected ids, skipping disabled/offline nodes with a per-node reason; exposed
as POST /panel/api/nodes/updatePanel (documented in endpoints.ts + openapi.json).
The bulk request sends a JSON body, so it sets Content-Type: application/json
explicitly — axios defaults POST to form-urlencoded, which made ShouldBindJSON
fail with 'invalid character i'.
Also reuses the clients-page online cue on the Nodes page: a pulsing green dot
plus green label for an online node. The .online-dot style moved to the shared
styles/utils.css so both pages load it.
Translations for all new node keys added across every language file.
Two small UX cues on the clients table online column:
- a pulsing green dot next to the Online tag so an active client reads as
live at a glance (honors prefers-reduced-motion).
- hovering the Offline tag shows the client's last-online timestamp from
record.traffic.lastOnline, formatted with the panel's calendar setting
(or "-" when the client has never connected).
The clients page summary counters (Online / Depleted / Depleting / Disabled
/ Active) came only from the paged-list response (staleTime: Infinity), so
they stayed frozen until a manual refresh or a mutation-triggered refetch —
the per-row columns updated over WebSocket but the summary card did not.
The client_stats WS event already broadcasts every client's traffic
(enable/up/down/total/expiryTime) every few seconds, so recompute the summary
client-side from it: computeClientsSummary mirrors the server's
buildClientsSummary, the latest event is stored in allClientStats, and the
summary is a useMemo over that plus the live onlines set. Falls back to the
server summary until the first event lands and keeps the server's
authoritative total. No extra polling, consistent with the existing
no-REST-fallback traffic design.
Auto-generated inbound tags (in-<port>-<l4>, n<id>- prefixed for node inbounds) now re-derive when port/listen/transport change on update instead of keeping the stale round-tripped value. The resolved tag is mirrored onto the API response, and NodeID is pinned to the stored row so a node inbound never loses its n<id>- prefix on edit. The edit form recomputes the tag live via a Go-parity helper so the JSON preview matches what gets saved.
Make node/central tag matching prefix-agnostic in all three places (traffic attribution, remote-id resolution, and the orphan sweep) so an n<id>- prefix present on only one side can no longer spawn duplicate inbounds or drop traffic on sync.
Force LF on shell scripts via .gitattributes (CRLF broke the Docker build shebang when the repo is checked out on Windows) and add a .dockerignore to keep node_modules/.git out of the build context.
Adds Go and frontend tests covering tag re-derivation, prefix-agnostic matching, and node-snapshot prefix mismatch.
ESLint failed the frontend build on four react-hooks/exhaustive-deps errors. Add the missing dependencies: the hysteria streamSettings effect now lists form, and the inbounds page prompt/import/general-action callbacks now list t. Both form (Form.useForm) and t (useTranslation) are stable references, so no extra re-renders or loops.
When an inbound save fails Zod validation, the toast previously showed a
raw path like `settings.clients.494.tgId: Invalid input`, which gave no
hint which of hundreds of clients was at fault. Resolve the client array
index back to the client email, name the field, and append a "(+N more)"
count when several fields fail. console.error now logs a readable list of
every issue instead of dumping the whole form.
Adds the invalidClientField/invalidField/moreIssues toast strings across
all 13 translations.
Drop the table header sorter on the inbounds page: the sortKey/sortOrder
state, the sortedInbounds memo and onChange handler, the per-column
sorterFor spreads, the SORT_FNS comparator map, and the now-unused
SortKey/SortOrder types. The list renders in DB order.
freedomToWire called Object.entries(s.fragment), but getFieldsValue(true)
returns freedom settings without a fragment object when the Fragment switch
is off (its sub-fields never register). That threw 'Cannot convert undefined
or null to object' and silently killed the save. Guard fragment with a
fallback so an unset value is treated as empty.
While verifying against xray-core's freedom config, also:
- add the missing userLevel field (schema, form schema, adapter, UI)
- fix noise applyTo enum to ip/ipv4/ipv6 (xray rejects the old host/all)
Closes#4686
- entity.go: tighten SessionMaxAge validate tag gte=0 -> gte=1 to match the panel UI (min 60) and the hand-written setting.ts schema
- GeneralTab.tsx: add max bounds to sessionMaxAge (525600) and pageSize (1000), raise pageSize min to 1
- regenerate zod.ts/types.ts, picking up pending drift: panelProxy field, client group field, InboundFallback.dest, and dropping the stale hysteria2 protocol enum value
Two PostgreSQL gaps on the panel:
1. x-ui setting and other CLI subcommands read XUI_DB_TYPE/XUI_DB_DSN from
the process environment, which systemd injects via EnvironmentFile but a
plain shell invocation does not. On a PostgreSQL install the CLI silently
fell back to SQLite, so changes made from the management menu never
reached the panel's database. Load the systemd EnvironmentFile
(/etc/default/x-ui and distro equivalents) at startup; godotenv.Load does
not override existing vars, so it stays a no-op for the managed service.
2. DB backup/restore (panel endpoints and the Telegram bot) only handled the
SQLite file, so on PostgreSQL Back Up returned a stale/absent x-ui.db and
Restore silently did nothing. Add pg_dump/pg_restore based backup/restore:
- GetDb/ImportDB run pg_dump (custom format) / pg_restore, passing
credentials via the PG* environment instead of argv.
- getDb downloads x-ui.dump on Postgres, x-ui.db on SQLite.
- Telegram backup sends the matching file via GetDb.
- BackupModal shows a Postgres note and accepts .dump; the dist page
injects window.X_UI_DB_TYPE; new strings translated for all locales.
- install.sh installs postgresql-client for the external-DSN path and
points the user to in-panel Backup & Restore.
Closes#4658
New peers were always seeded with allowedIPs 10.0.0.2/32, so each "Add
peer" reused the same address. Derive the next address from the highest
IPv4 already present across existing peers (max + 1, keeping its prefix),
falling back to 10.0.0.2/32 when there are no peers yet.
Closes#4682
The client create/edit form left `group` out of the request payload, so choosing a group in the form was silently dropped (bulkAdd from the Groups page still worked because it writes the column directly). Add `group` to the payload next to `comment`.
SyncInbound also overwrote group_name unconditionally; a group set via bulkAdd is never pushed to the node, so the next node snapshot — which lacks it — wiped the column. Keep group sticky (only overwrite when the incoming value is non-empty); group is only ever set/cleared via the Groups page. Preserve comment for node clients during snapshot sync the same way. Add tests.
The clients page saved searchKey and filters to localStorage but not
the sort selection, so leaving the page and returning reset sort to the
default (Oldest). Persist the chosen sort alongside the existing filter
state and restore it on mount, matching the filter-persistence pattern.
Mirror the clients page: checkbox selection on the desktop table and on
mobile cards, with a danger Delete button in the toolbar that removes all
selected inbounds in one call.
Backend adds POST /panel/api/inbounds/bulkDel, which loops the existing
DelInbound per id (xray restarts at most once) and returns {deleted,
skipped}. Frontend shows a confirm modal plus a result toast, clears the
selection on success, adds bulk-delete i18n keys across all 13 languages,
and documents the endpoint in the in-panel API docs.
Deleting an inbound now only detaches its clients (removes the
client_inbounds rows). It no longer deletes client_traffics or client IP
logs: those are keyed centrally by email (one row per client) and must
survive, since a client may stay attached to other inbounds and is
managed from the Clients page.
Separately, /get/:id now uses a new GetInboundDetail that preloads and
enriches ClientStats, so hydrated records (info / QR / export) carry
per-client traffic instead of null. DBInbound.toJSON drops the internal
_clientStatsMap cache so it no longer leaks into the exported JSON.
webBasePath, subPath, subJsonPath and subClashPath are URL paths, so '/'
stays allowed, but spaces, backslashes and control characters break
routing. Strip them as you type (shared sanitizePath helper, now also
applied to the panel base path) and reject them on save in
AllSetting.CheckValid so direct API callers are covered too.
Like the client email, the subId is embedded directly in subscription
URLs, so the same characters break it. Validate it on the backend
(Create + Update) and the frontend (Zod), with a localized message
across all 13 locales. An empty subId stays allowed (it is then
auto-generated).
Client emails containing a slash broke the path-param routes
(edit/delete/view returned 404 / "client not found"), leaving stale
records that could only be cleared with manual SQLite edits. Validate
the email on both the backend (Create + Update, which also covers the
bulk paths) and the frontend (Zod) so these characters are rejected at
save time with a clear, localized message across all 13 locales.
Closes#4695
* refactor(frontend): reorganize components & pages into feature folders
No behavior change; pure file relocation + import path updates.
* refactor(frontend): move shared protocol enums to schemas/protocols/shared
Decouple Outbound from Inbound schemas: SSMethodSchema and VmessSecuritySchema (shared between inbound & outbound) now live in a neutral schemas/protocols/shared/ module. Outbound no longer reaches into schemas/protocols/inbound/*. Pure relocation + import rewiring; schema values identical, snapshots & golden tests unchanged.
* refactor(frontend): break InboundList into helpers/types/RowActions/columns hook/stats modal
InboundList.tsx 781 -> 203 lines. Extracted pure helpers (network labels, sort fns, isInboundMultiUser), shared types, the row-actions menu/cell, the table columns hook, and the mobile stats modal into the list/ folder. Code moved verbatim; no behavior change. typecheck/lint/test/build green, 337 tests pass.
* refactor(frontend): extract InboundInfoModal helpers, types & buildInboundInfo
InboundInfoModal.tsx 1081 -> 836 lines. Moved the pure data helpers (network host/path readers, link-protocol check, copy/download/statsColor/IP formatting) plus all shared types and the buildInboundInfo data builder into info/helpers.ts and info/types.ts. The state-coupled render body is left intact (no React render tests to guard a deeper split). Code moved verbatim; no behavior change. All gates green, 337 tests pass.
* test(frontend): add React Testing Library + jsdom render-test harness
- vitest projects: node unit tests stay lean; new jsdom 'components' project runs *.test.tsx
- component setup: matchMedia/ResizeObserver/localStorage polyfills, react-i18next init, persian-calendar-suite stub (only used under jalali locale)
- smoke + field-label structure snapshots for Inbound & Outbound form modals
- establishes the regression net required before decomposing the oversized form modals
- 341 tests pass (337 unit + 4 component); typecheck/lint/build green
* test(frontend): per-protocol field-structure coverage for both form modals
- drive the protocol Select in jsdom and snapshot rendered Form.Item labels for every protocol
- 10 outbound + 10 inbound protocol states captured as the regression net for protocol-core extraction
- add robust select-driving helpers (test-utils) + post-test body cleanup (setup.components)
- 341 tests pass; typecheck/lint green
* refactor(frontend): extract OutboundFormModal constants & stream helpers
OutboundFormModal.tsx 2238 -> 2080. Moved the pure option arrays/sets and the stream-slice helpers (newStreamSlice, hysteriaStreamSlice, isMuxAllowed, buildAddModeValues) into outbound-form-constants.ts and outbound-form-helpers.ts. Per-protocol render snapshots unchanged -> verified no behavior change. typecheck/lint/build green.
* refactor(frontend): extract InboundFormModal advanced JSON editors
InboundFormModal.tsx 3129 -> 2863. Moved AdvancedSliceEditor and AdvancedAllEditor (the in-modal JSON slice/all editors) into advanced-editors.tsx along with their adapter-helper imports. Per-protocol render snapshots unchanged -> verified no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal loopback/blackhole/dns field blocks
Moved the outbound-only protocol field blocks (loopback, blackhole, dns) out of the modal render body into outbound-only-fields.tsx. First render-body extraction behind the per-protocol snapshot net: loopback/blackhole/dns snapshots unchanged -> verified no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal freedom field block
OutboundFormModal.tsx 2063 -> 1753. Moved the freedom protocol field block (domainStrategy, fragment, noises, finalRules) into outbound-freedom-fields.tsx. Verbatim relocation; freedom per-protocol snapshot unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal wireguard field block
OutboundFormModal.tsx 1753 -> 1622. Moved the wireguard protocol field block (address, keypair gen, domainStrategy, peers + allowedIPs) into outbound-wireguard-fields.tsx; dropped now-unused icon/InputAddon/WireguardDomainStrategy imports. Verbatim relocation; wireguard snapshot unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal core protocol fields
OutboundFormModal.tsx 1622 -> 1538. Moved the shared protocol core field blocks (vmess/vless ID, vmess security, vless encryption/reverseTag, trojan/ss password, ss method/uot, socks/http user/pass) into outbound-core-fields.tsx; dropped now-unused schema/option imports. Per-protocol snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): fold OutboundFormModal server address/port block into core fields
OutboundFormModal.tsx 1538 -> 1516. Moved the shared connect-target (address/port) block into OutboundCoreProtocolFields at the same render position; dropped the now-unused SERVER_PROTOCOLS import. Snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split outbound-only protocol forms into per-protocol files
Replace the grouped outbound-only-fields.tsx + outbound-freedom-fields.tsx with one file per protocol under outbounds/protocols/: freedom.tsx, blackhole.tsx, dns.tsx, loopback.tsx (+ barrel). Matches the prompt's 1-file-per-protocol structure. Outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split outbound protocol forms into per-protocol files
Replace the grouped outbound-core-fields / outbound-wireguard-fields with one file per protocol under outbounds/protocols/: vmess, vless, trojan, shadowsocks, http, socks, wireguard, freedom, blackhole, dns, loopback (+ shared server-target). Matches the prompt's 1-file-per-protocol structure (per-modal). Outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split outbound transport forms into per-transport files
Extract the tcp(raw)/kcp/ws/grpc/httpupgrade transport blocks into outbounds/transport/ per-file components (RawForm, KcpForm, WsForm, GrpcForm, HttpUpgradeForm). xhttp + hysteria transport remain inline for a follow-up. Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal xhttp transport form
Move the xhttp transport block into transport/xhttp.tsx (takes form + onXmuxToggle prop); drop now-unused HeaderMapEditor and MODE_OPTIONS imports from the modal. OutboundFormModal.tsx down to ~1001 lines (from 2238 originally). Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): extract OutboundFormModal tls/reality security forms
Move the TLS and Reality field blocks into outbounds/security/{tls,reality}.tsx; the none/TLS/Reality Radio.Group selector stays in the modal. Drop now-unused ALPN_OPTIONS/UTLS_OPTIONS imports. OutboundFormModal.tsx down to ~918 lines (from 2238 originally). Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split inbound-only protocol forms (tun, tunnel) into per-file
Extract the tun and tunnel protocol blocks from InboundFormModal into inbounds/form/protocols/{tun,tunnel}.tsx (presentational, declarative). First inbound-side per-protocol split. Verbatim relocation; inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split inbound wireguard & shadowsocks protocol forms
Extract the wireguard and shadowsocks protocol blocks from InboundFormModal into inbounds/form/protocols/{wireguard,shadowsocks}.tsx (presentational; form + regen handlers / isSSWith2022 passed as props). Drop now-unused Divider + SSMethodSchema imports. Verbatim relocation; inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): split inbound vless/http/mixed/hysteria protocol forms
Extract the remaining inbound protocol blocks into inbounds/form/protocols/: vless (auth handlers/state as props), http + mixed (shared accounts-list), hysteria. Drop now-unused HysteriaMasqueradeForm/Typography/Text imports from the modal. InboundFormModal.tsx 2841 -> 2478. Inbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
* refactor(frontend): move HysteriaMasqueradeForm to lib/xray/forms/transport
The hysteria masquerade form edits streamSettings.hysteriaSettings.masquerade (a transport/stream concept) and is rendered identically by both modals, so it belongs next to FinalMaskForm in lib/xray/forms/transport/ rather than protocols/shared/. Moved the file, updated the transport barrel + both consumers (inbound hysteria protocol form, outbound modal), and removed the now-empty protocols/shared/ folder. Pure relocation; snapshots unchanged, typecheck/lint/build green.
* refactor(frontend): extract inbound transport forms into transport/ folder
Move the six inbound stream-transport blocks (tcp/raw, ws, grpc, xhttp,
httpupgrade, kcp) out of InboundFormModal into presentational components
under inbounds/form/transport/. XhttpForm takes the form instance and
re-derives its mode/obfs/placement watches internally; the rest are
declarative. InboundFormModal drops from 2566 to 2105 lines. No behavior
change — per-protocol field-label snapshots unchanged.
* refactor(frontend): extract inbound security forms into security/ folder
Move the inbound TLS and Reality stream-security blocks out of
InboundFormModal into presentational components under
inbounds/form/security/. The Radio.Group security selector stays in the
modal; TlsForm and RealityForm receive their cert/key/ECH generation
handlers and the saving flag as props. InboundFormModal drops from 2105
to 1708 lines.
Add inbound-form-blocks.test.tsx: render-snapshot coverage for each
extracted transport (raw/ws/grpc/kcp/httpupgrade/xhttp) and security
(tls/reality) component in isolation inside a minimal Form. The full
modal cannot exercise the stream/security tabs in jsdom because they are
gated behind Form.useWatch values that do not propagate in the test
harness, so component-level snapshots are the regression net for these
blocks. No behavior change.
* refactor(frontend): extract outbound sockopt/mux/hysteria transport blocks
Move the last three oversized inline stream blocks out of
OutboundFormModal into presentational components under
xray/outbounds/transport/: SockoptForm (~260 lines, the worst offender),
MuxForm, and HysteriaForm. Each takes the form instance; MuxForm also
takes protocol/network and keeps its isMuxAllowed gate. OutboundFormModal
drops from 962 to 621 lines and no inline section now exceeds the
250-line guideline. Existing outbound-form-modal snapshots already cover
sockopt/mux and stay byte-identical, confirming no behavior change.
* refactor(frontend): extract inbound sockopt + external-proxy blocks
Move the inbound Sockopt (~250 lines) and External Proxy stream blocks
out of InboundFormModal into presentational components under
inbounds/form/transport/, mirroring the outbound extraction. Each takes
its toggle handler (toggleSockopt / toggleExternalProxy) as a prop and
keeps its render-prop getFieldValue gate. InboundFormModal drops from
1708 to 1332 lines.
Extend inbound-form-blocks.test.tsx with isolated render-snapshot
coverage for both (SockoptForm seeded enabled + happyEyeballs;
ExternalProxyForm seeded with one TLS entry). No behavior change.
* refactor(frontend): break down RoutingTab into sections
Extract RoutingTab's presentational pieces into the routing/ folder:
helpers.ts (arrJoin/csv/chipPreview/ruleCriteriaChips), types.ts
(RuleRow), CriterionRow.tsx, RuleCardList.tsx (mobile card view), and
useRoutingColumns.tsx (desktop table columns hook). RoutingTab stays the
orchestrator holding rule state, mutate, tag-option memos and the
pointer-drag reorder logic, and drops from 550 to 291 lines. No behavior
change.
* refactor(frontend): extract BasicsTab constants and rule helpers
Move BasicsTab's geo option arrays + freedom/ipv4 outbound presets into
basics/constants.ts and the routing-rule get/set/sync helpers into
basics/helpers.ts. BasicsTab drops from 550 to 447 lines and keeps its
Collapse-of-settings panels (which stay coupled to mutate + derived
state, so splitting them into components would only add prop-drilling).
No behavior change.
* refactor(frontend): break down DnsTab columns/helpers/types
Extract DnsTab's pure pieces into the dns/ folder: helpers.ts
(STRATEGIES/DEFAULT_FAKEDNS + addr/domains/expectedIPs accessors),
types.ts (DnsConfig/HostRow/FakednsRow), and useDnsColumns.tsx
(useDnsServerColumns + useFakednsColumns table-column hooks taking their
row handlers as params). DnsTab stays the orchestrator for dns state,
mutate, hosts sync and the Collapse panels, and drops from 539 to 424
lines. No behavior change.
* refactor(frontend): break down OutboundsTab into sections
Extract OutboundsTab's pieces: outbounds-tab-types.ts (OutboundRow),
outbounds-tab-helpers.ts (address/untestable/security/breakdown +
traffic/testing/result accessors), useOutboundColumns.tsx (desktop table
columns hook) and OutboundCardList.tsx (mobile card view). OutboundsTab
stays the orchestrator for outbound state, mutate, reorder and the
toolbar, and drops from 516 to 238 lines. No behavior change.
This completes plan section 2.4.5 — all four oversized Xray tabs
(Basics/Routing/Dns/Outbounds) are now broken into sections + hooks.
* refactor(frontend): fold HysteriaMasqueradeForm into the hysteria forms
Inline the masquerade fields directly into both hysteria transport forms
(inbounds/form/protocols/hysteria + xray/outbounds/transport/hysteria)
and delete the shared lib/xray/forms/transport/HysteriaMasqueradeForm so
each hysteria form is self-contained. The masquerade JSX is unchanged;
form is typed as the untyped FormInstance (as the shared component was)
so the masquerade name paths still resolve. No behavior change.
* refactor(frontend): slim InboundFormModal by extracting hooks + sections
Pull the modal's non-layout logic into focused files at the form root:
- useSecurityActions.ts: TLS/Reality key + cert generation handlers and
onSecurityChange (consumed by the security tab)
- useInboundFallbacks.ts: fallback row state + load/save/derive/add/
update/remove/move handlers + eligible-child options
- FallbacksCard.tsx: the fallbacks card UI (presentational)
- SniffingTab.tsx: the sniffing tab UI (presentational)
Also drop the stale "Pattern A rewrite / sibling file" header comment and
the imports the extractions made unused. InboundFormModal goes from 1332
to 868 lines with no behavior change (351 tests green, snapshots
unchanged).
The hysteria protocol now offers only the Hysteria transport (other transports removed) and security is always TLS. This prevents the broken hysteria-over-tcp / security:none outbounds that made xray-core fail to start with 'Failed to build Hysteria config. > version != 2'.
Show the fixed version field directly under Transmission, and expose the full masquerade sub-form on the outbound too. The masquerade UI was extracted into a shared HysteriaMasqueradeForm component used by both the inbound and outbound forms.
Closes#4665
Add an IP Log popup (view list + refresh + clear) to the client edit form and the Client Information modal, with IPs stacked vertically.
Identify inbounds by their xray tag (not remark/protocol:port) across every picker and chip: attach/detach modals, the attached-inbounds column and field, the filter drawer, and bulk-add. Add the tag field to the InboundOption schema (the backend already returned it).
Clarify modal titles/labels: Client Information (was More Information) and Inbound Information (was Inbound's Data); Client Information / QR Code titles now include the client email.
i18n: rename keys moreInformation->clientInfo and inboundData->inboundInfo with proper translations in all languages; addTitle->addClient, editTitle->editClient, addToGroupPlaceholder->groupName.
Add parseWireguardLink to the outbound import dispatcher: maps the secretKey userinfo, peer publicKey/endpoint, address, mtu, reserved, preSharedKey and keepAlive (probing common client aliases). Previously any wireguard:// link fell through to null and showed "Wrong Link!".
Also fix parseShadowsocksLink so a trailing query string (e.g. ?type=tcp) no longer leaks into the host:port slice, which made Number(port) NaN and silently fell back to 443. Strip the query before parsing in both the modern and legacy ss forms.
Outbound connection tester (#4657): UDP-based outbounds (wireguard,
hysteria, kcp/quic transports) were probed with a raw UDP dial that
treated the inevitable read timeout as success, so every one reported a
fake ~5s 'alive'. Route them through the authoritative xray
burstObservatory probe and drop the broken raw-UDP path. Test All now
runs a parallel TCP lane and a serial HTTP lane so xray-probe outbounds
don't collide on the test semaphore.
Vision testseed: the [900, 500, 900, 256] default repeats 900, and a
tags Select keys each tag by value -> 'two children with the same key,
900'. Render it as four InputNumbers (inbound + outbound forms); the
field is a fixed 4-tuple where repeats are valid.
Inbound form: drop the null-valued 'Local Panel' Select option (AntD
rejects null option values; placeholder + allowClear already cover it).
Outbound form: add an explicit 'None' option to the Flow selector.
- derive XMUX toggle from saved xmux on load, seed defaults on enable,
and drop xmux when disabled (#4654)
- save the JSON tab straight from parsed text so sockopt, finalmask (TCP
masks), mux, and reverse excludes round-trip instead of being dropped
by the form-store bounce
- remove the redundant Host/Path fields from HTTP obfuscation that fought
the request.headers editor over the same form path
- rebuild the outbounds table columns on row content change (rows, not
rows.length) so a re-opened edited outbound shows fresh values
- add adapter round-trip regression tests
Closes#4654
Xray's freedom outbound accepts a numeric proxyProtocol (0 disabled,
1 or 2 for the PROXY protocol version), but the panel had no field for
it and the typed form adapter dropped the key on save — so a value set
via the JSON editor disappeared the moment the outbound was saved.
Model proxyProtocol through the freedom wire schema, the form schema,
and both adapter directions (clamped to 0/1/2, omitted from the wire
when 0), and add a Select (none / v1 / v2) to the freedom section of
the outbound form. Add round-trip test coverage and the proxyProtocol
label across all locales.
Closes#4486
Opening the client sublinks/QR modal crashed when a link used
post-quantum keys (ML-DSA-65 / ML-KEM-768): the encoded URL exceeds
the antd QRCode capacity and the component throws. The client QR modal
rendered the QRCode unconditionally, so it took down the page.
The names don't appear verbatim in a share link — mldsa65Verify rides
inside pqv=<base64> and ML-KEM-768 inside encryption=mlkem768x25519plus.
The QR modal and inbound QR modal used a literal-substring guard that
missed those encoded forms, leaving the QR (and the crash) in place.
Consolidate detection into a single isPostQuantumLink() helper in
inbound-link.ts and reuse it across the client QR, inbound QR, client
info, and sub surfaces. The copy/download link still works; only the
QR image is suppressed for oversized post-quantum links.
Closes#4656
The clients list returns slim rows without secrets (uuid/password/auth)
or flow/security/tgId/reset/group. setEnable built its update payload
straight from the slim row, sending an empty id, so the backend treated
it as a new client and regenerated the UUID (and dropped the omitted
fields). Hydrate the full record first and send a complete payload that
changes only the enable flag.
OutboundFormModal.onOk built the save payload from form.validateFields(),
which only returns REGISTERED Form.Item values. The security selector is a
Radio.Group that writes streamSettings.security via setFieldValue with no
bound Form.Item, so validateFields() dropped it — network, tlsSettings and
realitySettings (all registered) survived, but the security discriminator
vanished and xray-core fell back to security="none". This hit both new
outbounds and re-saved ones.
Read the full form store with getFieldsValue(true) for the payload (still
validating first), matching how the inbound modal already does it.
Closes#4634