Commit Graph

61 Commits

Author SHA1 Message Date
MHSanaei 21e9b94bb4 fix(sub): emit Shadowsocks http-header links as SIP002 obfs-local plugin
v2rayN's SS parser only reads the SIP002 `plugin` query param; it ignores the
xray-native type/headerType/host/path, so an SS link with a TCP http header
imported as plain SS and failed to connect. Re-encode the http header as
`plugin=obfs-local;obfs=http;obfs-host=<host>`, which v2rayN maps to an
xray tcp/http-header outbound. Mirrored in the frontend link generator.

Note: v2rayN carries only the host and forces request path "/", so this matches
an inbound whose header path is "/" (the default); xray validates path, not host.
2026-06-17 14:11:25 +02:00
Sanaei 709b332d17 feat(hosts): managed Hosts for per-host subscription link overrides (#5409)
* test(sub): characterize current link output (externalProxy + single-link baselines)

Phase 0 of the Hosts feature. Locks current subscription-link output for the
externalProxy paths (vless/vmess/trojan/ss exact, reality/hysteria by Contains)
so the upcoming ShareEndpoint refactor can be proven behavior-preserving. These
must stay green and unedited through every later phase.

* refactor(sub): unify external-proxy link building behind ShareEndpoint (TDD, snapshot-locked)

Phase 1 of the Hosts feature. Collapse the duplicated externalProxy link
builders (param-form for vless/trojan/ss, object-form for vmess) onto a single
ShareEndpoint abstraction so Phase 4 can add Host-driven links with ~zero new
branching.

Design: an externalProxy-derived endpoint carries the original entry map and
applies it through the UNCHANGED applyExternalProxyTLS{Params,Obj} helpers, so
output is provably byte-identical. buildExternalProxyURLLinks /
buildVmessExternalProxyLinks become thin adapters; the genVless/Trojan/SS/Vmess
call sites are untouched. genHysteriaLink is deliberately left on its own path
(hex pinSHA256, not pcs). The no-externalProxy default tails are unchanged.

TDD: N1-N4 (externalProxyToEndpoint, inboundDefaultEndpoint, buildEndpointLinks,
buildEndpointVmessLinks) written failing-first against stubs, then implemented.

Mutation sanity (performed + reverted): dropping the ep-carry in
externalProxyToEndpoint makes the Phase-0 C1/C2 characterization snapshots go
red (TLS overrides vanish), proving the snapshots guard the emitted output.

Gate: go test ./internal/sub/... and go test ./... green with ZERO edits to the
Phase-0 snapshots; go build ./... green on linux and windows; go vet clean.

* feat(model): Host entity + automigrate + openapi codegen (TDD)

Phase 2 of the Hosts feature. Adds the Host GORM model: an override endpoint
attached to an inbound (address/port + TLS/transport/clash overrides + sub
scoping), superseding the legacy externalProxy array functionally while leaving
it intact.

- model.Host with snake_case column tags, json serializer for slices, text for
  free-JSON (mux/sockopt/xhttp), validate tags (remark 1-40, port 0-65535,
  security + mihomoIpVersion enums); TableName "hosts". NodeGuids column is added
  now but unused (host->node scoping deferred to v2).
- Registered in BOTH initModels() (db.go) and migrationModels() (migrate_data.go);
  the latter is required for cross-DB migration and is easy to miss. PG sequence
  resync iterates the initModels slice, so it is covered automatically.
- pruneOrphanedHosts() deletes hosts whose inbound_id has no inbound, called
  alongside pruneOrphanedClientInbounds().
- openapigen manifest: Host added to StructAllow with MuxParams/SockoptParams/
  XhttpExtraParams -> KindAny; regenerated frontend/src/generated/* + openapi.json.

TDD: TestHostTableName, TestHostValidation, TestHostAutoMigrateCreatesColumns
(+ _Postgres), TestPruneOrphanedHosts written failing-first against a wrong-name,
untagged, unregistered stub, then implemented.

Gate: go test ./... green on SQLite AND a real Postgres DSN (local container);
go build/vet/gofmt clean; npm run gen succeeds with the new Host type/schema/
example/zod; npm run typecheck + npm run test (542) green.

* feat(api): Host CRUD service + controller + routes (TDD)

Phase 3 of the Hosts feature.

- service/host.go (HostService, empty struct + database.GetDB() like
  ClientService): GetHosts, GetHostsByInbound, GetHost, AddHost (verifies the
  inbound exists — no hard FK), UpdateHost (inbound + sort order immutable here),
  DeleteHost, SetHostEnable, SetHostsEnable, DeleteHosts, ReorderHosts (single
  driver-safe transaction), GetAllTags.
- controller/host.go mirrors NodeController: routes under /panel/api/hosts
  (list/get/byInbound/tags + add/update/del/setEnable/reorder + bulk/setEnable,
  bulk/del), binds via middleware.BindAndValidate so the model validate tags are
  enforced, {success,msg,obj} envelopes.
- Wired the hosts group into api.go after nodes (inherits checkAPIAuth + CSRF).
- DelInbound now cascades: deleting an inbound deletes its hosts.
- Documented all 11 routes in api-docs endpoints.ts (referencing the generated
  Host schema) and regenerated openapi.json; extended TestAPIRoutesDocumented's
  controller->basePath switch for host.go. Backend en toast keys added.

TDD: service tests (Add/GetByInbound, RejectsUnknownInbound, Reorder, Set/Bulk
enable, DeleteHosts, DeleteInboundCascadesHosts, GetAllTags) written failing-
first against a nil-returning stub; controller test (AddListGetDelete envelope
round-trip + AuthInherited 401) added.

Gate: go test ./internal/web/... + go test ./... green; npm run gen + typecheck
+ lint + test (542) + build green.

* feat(sub): render subscription links from hosts; legacy fallback when none (TDD, mutation-checked)

Phase 4 of the Hosts feature. Inserts host resolution between inbound and link
across all three subscription formats.

Mechanism: hostEndpoints(inbound, format) loads the inbound's enabled hosts
(filtered by ExcludeFromSubTypes, ordered by sort_order then id) and projects
each onto the externalProxy entry shape the raw/json/clash renderers already
consume. So a host fans out one link/proxy reusing the exact existing rendering
(address/port/security/sni/fp/alpn/pins/ech) with zero new TLS code. Host header
and path overrides are applied additively in the raw builders (no-op for legacy
externalProxy, which never carries those keys — characterization snapshots stay
green). Clash ip-version (MihomoIpVersion) is set last on the proxy.

Integration points:
- getSubs (raw): per inbound, hostEndpoints AFTER projectThroughFallbackMaster;
  len>0 -> linkFromHosts (renders only the hosts), else legacy GetLink.
- GetJson/GetClash: inject the host endpoints into the inbound's externalProxy
  before the existing getConfig/getProxies loop.
- Precedence: hosts win over any legacy externalProxy (injection replaces it).

Backward compat: a zero-host inbound takes the legacy path -> byte-identical
output (all Phase-0 characterization snapshots unchanged).

TDD: 9 cycles (zero-hosts identical, N-links-ordered with host/path override,
disabled skipped, host-vs-externalProxy precedence, no-dedup, sort composes with
SubSortIndex, host-over-fallback, resolve-via-client-inbounds, ExcludeFromSubTypes
per format) written failing-first against unwired helpers, then wired green.

Mutation sanity (performed + reverted, documented here):
- zero-hosts fallback: flipping the len(hostEps)>0 guard to >=0 makes
  TestSub_ZeroHosts_IdenticalOutput go red (host path yields "" for no hosts).
- no-dedup: adding a remark-dedup in hostEndpoints makes TestSub_NHosts_NoDedup
  go red (two distinct hosts collapse to one link).

Gate: go test ./internal/sub/... + go test ./... green with ZERO edits to the
Phase-0 snapshots; go build green on linux and windows; go vet + gofmt clean.

* feat(migration): seed hosts from inbound externalProxy (TDD, idempotent, dual-driver)

Phase 5 of the Hosts feature. One-time migration so existing installs surface
their legacy externalProxy entries as first-class Host rows.

- seedHostsFromExternalProxy() is self-gated on a HistoryOfSeeders
  "HostsFromExternalProxy" row (run-once) and wired into runSeeders. For each
  inbound it parses StreamSettings, reads externalProxy[], and creates one Host
  per entry: forceTls->Security (unknown->same), dest->Address, port->Port,
  remark->Remark (generated when blank, capped at 40), sni/fingerprint/alpn/
  pinnedPeerCertSha256/echConfigList copied; SortOrder=index; InboundId set.
- Additive: externalProxy is left intact in StreamSettings (rollback-safe; the
  sub layer prefers hosts when present, §Phase 4).
- Postgres: GORM db.Create advances hosts_id_seq via the sequence, so no extra
  resync is needed beyond the existing startup resync.

TDD: field-mapping, idempotency (second run no-op), no-externalProxy->no-hosts,
externalProxy-kept-intact written failing-first against a stub; plus a
Postgres counterpart that skips without XUI_DB_DSN.

Gate: go test ./internal/web/service/... ./internal/database/... green on SQLite;
the *_Postgres tests green against a real Postgres container; go build green on
linux and windows; go vet + gofmt clean. (Running the whole database package
under XUI_DB_TYPE=postgres is not supported — the SQLite-path tests share the one
DSN — so only the t.Skip-gated *_Postgres tests run with the env set.)

* feat(ui): Hosts page + schema + query hooks + link preview helper (TDD on schema/helpers)

Phase 6 of the Hosts feature — the admin UI.

- schemas/api/host.ts: HostFormSchema (validation: remark 1-40, tags ^[A-Z0-9_:]+$
  ≤10×≤36, port 0-65535, security/mihomoIpVersion enums, alpn/fingerprint reused
  from the shared primitives) + a loose HostRecordSchema/HostListSchema for reads.
- lib/hosts/host-link.ts: hostToExternalProxyEntry — the frontend mirror of the
  backend hostToExternalProxyMap (security->forceTls, sni override rules, port
  inherit), for share-link previews.
- api/queries/useHostsQuery.ts + useHostMutations.ts (mirror the node hooks):
  list/get + add/update/del/setEnable/reorder/bulk; queryKeys.hosts.* added;
  mutations invalidate keys.hosts.root().
- pages/hosts/{HostsPage,HostList,HostFormModal}.tsx (+CSS) mirroring pages/nodes:
  list with remark · address:port · inbound · security · tags · enable Switch ·
  per-inbound move up/down (reorder) · bulk enable/disable/delete; form grouped
  into Basic / Advanced / Clash / Subscription-scope sections.
- Route '/hosts' + sidebar item (Global icon); menu.hosts + pages.hosts.* added to
  the en-US bundle (other locales fall back to English until translated).

TDD: HostFormSchema (10 cases) and hostToExternalProxyEntry (6 cases) written
failing-first, then implemented. UI verified by lint/typecheck/test/build.

Deferred (documented enhancement): the live in-form share-link preview (needs
inbound+client context) and a per-host host/path override in JSON/Clash output
(raw already overrides; JSON/Clash inherit the inbound's host/path).

Gate: cd frontend && npm run lint && npm run typecheck && npm run test (557) &&
npm run build all green; go build ./... + go test ./... still green.

* refactor(ui): remove the External Proxy form from the inbound stream settings

Hosts supersede the legacy externalProxy: the subscription renders from hosts
(hosts win when both exist) and the migration converts existing externalProxy
entries to hosts. externalProxy's only real consumers were the subscription
(now covered) and this form's preview — the backend per-client copy-link never
used it — so removing the editor has no functional regression.

- Drop ExternalProxyForm + toggleExternalProxy from InboundFormModal and delete
  the orphaned form component + its export; remove its block test + snapshot.
- KEEP the externalProxy schema field and backend parsing/link-generation: an
  existing inbound's externalProxy still round-trips through the form (not
  silently destroyed on edit) and still renders if a host was removed.

Gate: cd frontend && npm run typecheck + lint + test (556) + build green.

* fix(ui): use Alert `title` instead of deprecated `message` (antd 6)

Ant Design 6 deprecated <Alert message=> in favor of <Alert title=>; the panel
was mid-migration (21 Alerts already on title). Renamed the 7 remaining stragglers
across 5 files (SubLinksModal, InboundFormModal, sockopt, EmailTab, TelegramTab),
silencing the runtime deprecation warning. description= is unchanged.

Pre-existing warning, surfaced while testing Hosts — not introduced by it.

Gate: npm run typecheck + lint + test (556) + build green.

* style(ui): align Hosts page with Clients/Inbounds cards + reorder columns

- page-shell.css never listed .hosts-page, so the Hosts page got no content
  padding / transparent-layout / summary-card spacing. Add a .hosts-page shell
  block (background, dark/ultra vars, content-area + summary-card padding). This
  is the actual "card spacing" bug.
- HostList: match the Clients/Inbounds list card — hoverable + the toolbar moved
  into the card title as a .card-toolbar (Add when nothing selected; selected
  count + bulk enable/disable/delete on selection). Re-declare .card-toolbar in
  HostList.css since the shared rule lives in a lazily-loaded page stylesheet.
- Reorder table columns as requested: Actions, Enable, then Remark, Endpoint,
  Inbound, Security, Tags. Added scroll x for narrow screens.
- HostsPage: add a summary card (Total / Enabled / Disabled) like the other
  pages. New i18n keys: pages.hosts.selectedCount + pages.hosts.summary.*.

Gate: npm run typecheck + lint + test (556) + build green.

* style(ui): use Tabs instead of Collapse in the Add/Edit Host form

The Basic / Advanced / Clash / Subscription-scope sections are now tabs. Each
pane sets forceRender so all fields stay mounted — required because the form
uses preserve=false, so an unmounted tab's values would otherwise be dropped on
submit (and a required field on a hidden tab still blocks submit).

Gate: npm run typecheck + lint + test (556) + build green.

* style(ui): split Host form into Security + Advanced tabs; drop unused JSON fields

- Remove the Mux/Sockopt/XHTTP raw-JSON fields from the Host form: they were not
  wired into link generation and the inbound's structured editors are inbound-
  specific (not reusable). The DB columns + read schema + generated type stay, so
  they can get proper editors later. (HostFormSchema drops them; HostRecordSchema
  keeps them.)
- Reorganize tabs to Basic / Security / Advanced / Clash / Subscription scope:
  Security holds the TLS/cert fields (security, sni, sni-overrides, alpn,
  fingerprint, pins, verify-by-name, ech); Advanced now holds the transport
  overrides (host header, path).
- i18n: add pages.hosts.sections.security; drop the 3 unused field labels.

Gate: npm run typecheck + lint + test (556) + build green.

* style(ui): restore Mux/Sockopt/XHTTP fields in the Host Advanced tab

Put the three free-JSON override fields back, in the Advanced tab next to host
header / path (as JSON inputs — the inbound's structured editors aren't reusable
here). Re-added to HostFormSchema + defaults + the i18n labels.

Gate: npm run typecheck + lint + test (556) + build green.

* feat(hosts): add allowInsecure (rendered) + serverDescription/mihomoX25519/vlessRouteId fields

Closes most of the Remnawave-host gap analysis.

- model.Host: + allowInsecure, serverDescription (≤64), vlessRouteId (0-65535),
  mihomoX25519. Auto-migrated (SQLite + Postgres verified); openapi regenerated.
- allowInsecure is fully RENDERED into subscription output (TDD):
  - raw link: allowInsecure=1 (TLS/Reality, skipped for none) via the endpoint
    builder;
  - JSON/Clash: applyExternalProxyTLSToStream writes tlsSettings.settings.
    allowInsecure, and clash applySecurity now emits skip-cert-verify for the tls
    case (it previously only did so for Hysteria — a pre-existing gap, so inbound
    allowInsecure now renders for vless/trojan/ss clash too).
- Frontend: the four fields added to the Host form (allowInsecure → Security,
  serverDescription → Basic, vlessRouteId → Advanced, mihomoX25519 → Clash);
  serverDescription shown under the remark in the list. Schema + i18n updated.

serverDescription / vlessRouteId / mihomoX25519 are stored + editable; their
deeper rendering (and per-host mux/sockopt/xhttp into JSON/Clash, plus a per-host
xray JSON template) are tracked as follow-ups.

Gate: go test ./... green (SQLite + Postgres for the host schema/migration);
go build linux+windows; go vet + gofmt clean; npm run gen + typecheck + lint +
test (556) + build green; generated files in sync.

* feat(sub): render host sockopt + xhttp-extra params into JSON/Clash output (TDD)

A host's sockoptParams and xhttpExtraParams (free-JSON) now take effect:
applyHostStreamOverrides injects sockopt into the per-host stream (re-added since
the base stream strips it) and merges xhttpExtraParams into xhttpSettings, called
in both getConfig (JSON) and getProxies (Clash) right after the per-host TLS
apply. No-op for legacy externalProxy entries (keys absent) — characterization
snapshots unchanged.

mux rendering is outbound-level (overrides outbound.Mux) and needs a genVless/
genVnext/genServer signature change — deferred, along with the per-host xray
JSON template.

Gate: go test ./internal/sub/... + go test ./... green (snapshots unchanged);
go build + vet + gofmt clean.

* feat(sub): render host muxParams as a per-host JSON outbound mux override (TDD)

genVnext/genVless/genServer take a muxOverride: a host's muxParams (when valid
JSON) overrides the global mux on its JSON outbound; empty falls back to the
panel mux (behavior unchanged for non-host configs). Completes the host
mux/sockopt/xhttp trio. Test call sites updated for the new signature.

Gate: go test ./internal/sub/... + go test ./... green (snapshots unchanged);
go build + gofmt clean.

* style(ui): show Host security fields conditionally per security (like externalProxy)

* feat(sub): apply host SNI + fingerprint override for reality (TDD)

A reality host now overrides SNI and fingerprint while inheriting publicKey/
shortId from the inbound (reality keys can't be host-supplied). Previously the
reality link kept the inbound's serverName because the TLS appliers are gated to
security=="tls".

- raw: applyEndpointRealityParams sets sni/fp on the params for reality;
- JSON/Clash: applyHostStreamOverrides sets realitySettings.serverName +
  serverNames from the host SNI.

Gated to host endpoints via an isHost marker on the synthesized ep, so the legacy
externalProxy path stays byte-identical (characterization snapshots unchanged).
The marker is internal and never emitted.

Gate: go test ./internal/sub/... + go test ./... green; go build + vet + gofmt clean.

* fix(ui): start the Host inbound select unselected instead of showing 0

A new host left inboundId defaulting to 0, so the Select rendered "0". inboundId
is now optional in the form (undefined until chosen), so it shows its
placeholder ("Select an inbound"); the required rule still enforces a choice on
save. Port keeps 0 (means "inherit the inbound's port").

Gate: npm run typecheck + lint + build green.

* fix(ui): drop redundant :port suffix from the Host inbound select label

The inbound tag (e.g. in-59303-tcp) already carries the port, so the appended
":59303" was duplicated. Show just the remark/tag.

Gate: npm run typecheck + lint + build green.

* style(ui): apply the shared card hover shadows to the Hosts page

page-cards.css scoped its card styling + hover shadows to each page class but
not .hosts-page, so Hosts fell back to antd's default hoverable (a larger/blurry
shadow + pointer cursor). Add a .hosts-page block matching the other pages.

Gate: npm run build green.

* feat(hosts): move Tags to Basic tab, add Nodes field, accept VLESS route ranges

- Move the Tags field into the Host form's Basic tab and add a Nodes
  multi-select (visual-only assignment, backed by the existing node_guids
  column) so the Basic tab matches the reference layout.
- Replace the single-port vlessRouteId integer with a free-form vlessRoute
  string that accepts comma-separated ports/ranges (e.g. 53,443,1000-2000);
  format-validated on the frontend, stored verbatim on the backend.
- Regenerated frontend types/openapi from the changed model.

* feat(hosts): structured editors for Mux/Sockopt/XHTTP + new Final Mask

Replace the raw JSON textareas in the Host form's Advanced tab with the same
structured editors used elsewhere, under a nested tabbed layout (General / Mux /
Sockopt / XHTTP / Final Mask), mirroring the Sub-JSON settings tab:

- Mux: the Sub-JSON mux editor (enable + concurrency/xudpConcurrency/xudp443).
- Sockopt + XHTTP: reuse the outbound SockoptForm / XhttpForm, wrapped in an
  isolated form that serializes the edited subtree back to the host's JSON
  string (pruned so the override stays sparse).
- Final Mask: new host field (model + column + JSON-render wiring that merges
  the masks into the host's JSON-subscription stream), edited via the shared
  FinalMaskForm like the Sub-JSON Final Mask editor.

Each editor stays a controlled value/onChange component bound to its existing
host JSON string field; backend rendering of mux/sockopt/xhttp is unchanged.

* feat(hosts): drop XHTTP + Xray-JSON-template overrides; fix mobile form layout

Remove the host's XHTTP extra-params and Xray-JSON-template overrides entirely
(model fields + columns, JSON-subscription render paths incl. hostTemplateOutbound,
schema, form tab/field, i18n, openapi codegen, and their tests) — they did not
fit the host model. Mux, Sockopt and Final Mask stay as structured editors.

Mobile fixes for the Edit Host modal:
- responsive width (95vw on mobile, was a fixed 760px that overflowed the
  viewport and clipped the tabs/labels) + a scrollable body so the footer stays
  on screen;
- Mux fields use responsive Row/Col (stack on mobile) instead of a fixed-width
  label grid.

* fix(hosts): hide the spurious horizontal scrollbar in the Edit Host modal

Setting overflowY:auto on the modal body forced overflow-x to auto too (CSS
rule), so antd Row's negative gutter margins triggered a horizontal scrollbar.
Pin overflowX:hidden.

* feat(hosts): inbound-style responsive field layout + icon empty state

- Host form (main form + Mux/Sockopt/Final Mask editors) now use the inbound
  form's label layout: label beside the input on desktop (labelCol sm span 8 /
  wrapperCol sm span 14, right-aligned), stacked label-above-input on mobile.
  Rewrote HostMuxForm onto an internal antd Form so it follows the same layout
  instead of a manual grid.
- Empty hosts table now shows the host icon + the shared 'Nothing here yet'
  (noData) text, matching Nodes/Inbounds/Clients, replacing the bespoke
  'No hosts yet…' string.

* fix(hosts): avoid nested <form> in the Edit Host modal

The Mux/Sockopt/Final Mask editors each render their own antd Form inside the
host's main Form, producing an invalid nested <form> DOM node (hydration
warning). Render those inner forms with component={false} so they keep the form
instance/context but emit no <form> element.

* fix(hosts): make the Mux enable toggle work

The Switch's checked state came from Form.useWatch('mux'), but the mux object
field had no registered Form.Item while disabled, so setFieldValue never
notified the watcher and the toggle stayed off. Bind the Switch to a real
name='enabled' field (antd drives its checked state directly) and keep the
sub-fields registered via hidden={!enabled}, serialized to the flat mux JSON.

* refactor(hosts): reuse the outbound MuxForm instead of a bespoke Mux editor

The Mux fields duplicated the outbound MuxForm. Reuse it through the same
wrapper as Sockopt: generalize OutboundSubtreeJsonForm with defaultSubtree
(pre-fill on enable) and a serialize hook, and have HostMuxForm render MuxForm
at the ['mux'] path. The host keeps its inherit-when-off semantics by storing ''
unless mux.enabled. Also drops the now-unused enableSwitch path from the
wrapper (only the removed XHTTP editor used it).

* style(hosts): use default-width Port input like the inbound form

The host Port used width:100% (full width); the inbound's numeric inputs use
antd's default width. Drop the override so Port matches. The Mux number inputs
already use the default width via the reused MuxForm.

* refactor(sockopt): readable customSockopt editor as a shared component

The customSockopt rows were a single cramped Space.Compact line and duplicated
verbatim in the inbound and outbound sockopt forms. Extract a shared
CustomSockoptList that renders each entry as a titled group of labeled fields
(System / Level / Opt / Type / Value), matching the rest of the form, and use it
in both (and thus the host Sockopt editor).

* fix(finalmask): drop the empty Custom Tables tag on a new sudoku mask

The sudoku TCP-mask default seeded customTables: [''] (one empty string), which
rendered as a blank removable tag. Seed [] instead.

* fix(sockopt): make the outbound (and host) Sockopt client-only

Per the XTLS sockopt docs, tproxy / acceptProxyProtocol / V6Only /
trustedXForwardedFor only apply to an inbound (listening socket); they are
meaningless on an outbound/dialer. Drop them from the outbound SockoptForm
(which the host reuses). The Sockopt default object still seeds those keys, so
the host also strips them on serialize, keeping its override honest to the
server/client split. The inbound SockoptForm is left unchanged.

* fix(sockopt): make the inbound Sockopt server-only

Complete the server/client split: drop the outbound/dialer-only fields from the
inbound SockoptForm — dialerProxy, domainStrategy, interface, addressPortStrategy,
happyEyeballs, tcpMptcp (client-only since Go 1.24 auto-enables MPTCP on listen).
mark stays (xray applies SO_MARK on inbound sockets too). Update the form-blocks
snapshot to the server-side field set (intentional spec change).

* feat(hosts): populate Sockopt dialerProxy with the panel's outbound tags

The host Sockopt editor reused the outbound SockoptForm with outboundTags=[],
so the dialerProxy dropdown was empty. Feed it the panel's outbound tags via
the existing useOutboundTags hook (shares the cached xray-config query;
blackhole excluded), so a host can chain through a subscription outbound by tag.

* fix(hosts): empty-state styling on direct load + exclude balancers from dialerProxy

- .card-empty was only defined in lazily-loaded Clients/Inbounds/Nodes
  stylesheets, so a direct /hosts refresh rendered the empty table state
  unstyled (faint + uncentered) until another page was visited. Re-declare it
  in HostList.css so it's correct on first load.
- The Sockopt dialerProxy dropdown listed balancer tags (useOutboundTags merges
  them in for mtproto egress). dialerProxy chains a single outbound, so balancers
  aren't valid — switch to useOutboundTagGroups and use only the outbound group.

* fix(outbounds): icon + 'Nothing here yet' empty state; stop fading other pages

The Outbounds empty state was a faint '—', and OutboundsTab.css set the global
.card-empty to opacity:0.4 — which leaked onto whichever page's empty state was
shown after the Outbounds CSS had loaded (e.g. Hosts went faint after visiting
Outbounds). Render the icon + noData ('Nothing here yet') like the other lists,
and align .card-empty to the shared centered/secondary style (no opacity).

* fix(outbounds): custom empty state on the desktop table too

The desktop Outbounds Table had no locale.emptyText, so it showed antd's
default 'No data' box. Add the same ExportOutlined + noData empty state as the
card (mobile) view.

* style(sidebar): use ExportOutlined for the Outbounds nav item

The Outbounds sidebar item used UploadOutlined (an upload tray). Switch to
ExportOutlined, matching the outbound icon now used in the routing target and
the outbounds empty states.

* feat(hosts): icons on the form tabs (icon-only on mobile)

Wrap every Host form tab label (Basic/Security/Advanced/Clash/Subscription
scope and the nested General/Mux/Sockopt/Final Mask) with catTabLabel, so the
tabs show icon + text on desktop and just the icon (with a tooltip) on mobile,
matching the Settings/Xray tab bars.

* refactor(hosts): fold Exclude-from-formats into Advanced, drop the one-field tab

The Subscription scope tab held only excludeFromSubTypes after Tags moved to
Basic — a niche per-format scoping knob. Move it into the Advanced > General
sub-tab and remove the standalone tab (and its now-unused subScope label/icon).

* feat(sub): per-client remark template variables; drop the remark model & Show Usage Info

* fix(migration): cap seeded host remark at the model's 256-char limit, not 40
2026-06-17 12:06:55 +02:00
MHSanaei 9385b6c609 feat(nodes): per-node client IP attribution for IP-limit
Record each panel's own Xray IP observations under its panelGuid and merge each node's guid-keyed report on the master, so the panel can tell which node a client IP is connecting through (the flat inbound_client_ips union is pushed back to every node and cannot attribute). Adds the NodeClientIp model + migration, the clientIpsByGuid endpoint and node-sync merge, node-name labels in the client IP log, and cleanup on node deletion.
2026-06-15 23:50:05 +02:00
MHSanaei c1fbfd0510 fix(outbound): parse xmux from imported share links (#5353)
The inbound link generator bundles xmux and downloadSettings as nested
objects inside the `extra=` JSON blob, but the outbound link parser only
pulled scalar fields and headers from it, silently dropping xmux on
import. Extract the nested objects too so they round-trip into the
outbound XMUX sub-form.
2026-06-15 19:12:47 +02:00
MHSanaei f00512d12e fix(frontend): TProxy schema, VLESS+XHTTP flow links, clearable Jalali date picker (#5339, #5322, #5313)
- #5339: accept transportless tunnel/TProxy streamSettings that carry no
  `security` key by adding a transportless branch to SecuritySettingsSchema,
  mirroring NetworkSettingsSchema. Fixes "streamSettings.security Invalid input".
- #5322: emit XTLS Vision `flow` in panel VLESS share links for XHTTP+vlessenc
  via the shared canEnableTlsFlow predicate, so panel links match the form and
  the subscription output.
- #5313: give the Jalali expiry date picker a working clear (X) button
  (remount on clear, since the library reads `value` only on mount) and a blank
  placeholder instead of the library's hardcoded Persian text.
2026-06-15 17:20:54 +02:00
Rouzbeh† dab0add191 feat(finalmask): support Salamander packetSize (Gecko) and Realm tlsConfig for Hysteria2 (#5278)
* feat(finalmask): support salamander packetSize (Gecko) and realm tlsConfig

Hysteria v2.9.1/v2.9.2 added two finalmask features that the pinned
Xray-core (26.6.1, 94ffd50) already supports but the panel UI did not
expose: Salamander's packetSize range (Gecko, XTLS/Xray-core#6198) and
the Realm UDP hole-punching mask's optional tlsConfig (XTLS/Xray-core#6137).

Add typed schemas and form fields for both, keeping UdpMaskSchema.settings
permissive per the existing finalmask design note. packetSize reuses the
existing dash-range preprocess (like udpHop.ports) so it round-trips under
the fm= share-link param with no new URI key; realm tlsConfig emits xray's
flat TLSConfig shape (serverName/alpn/fingerprint/allowInsecure).

Verified against the bundled Xray 26.6.1: configs with packetSize and
realm tlsConfig validate (Configuration OK.), plain salamander stays
backward-compatible, and a malformed packetSize is correctly rejected by
the salamander mask builder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(finalmask): add snapshots for salamander-gecko and realm-tls fixtures

vitest run does not auto-create missing snapshots in CI mode, so the two
new fixtures need committed snapshot entries. Verified under node:22 that
finalmask.test.ts passes (6/6) with these snapshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(finalmask): polished Gecko UX with core-grounded validation

Fold PR #5281's Gecko work into the Realm tlsConfig base:

- Replace the plain packetSize input with a Salamander/Gecko mode
  selector and validated Min/Max number inputs.
- parseGeckoPacketSize enforces xray-core's real bound
  (1 <= min <= max <= 2048, the gecko buffer size) so the panel
  rejects configs core would reject at runtime.
- Accurate Gecko description; add parser unit tests.
- Drop the unused Salamander/Realm settings schemas; settings stay
  permissive and are validated at the form level.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-15 00:21:31 +02:00
Nikan Zeyaei 7c737820d1 fix(links): bracket ipv6 hosts in share links and qr codes (#5310)
* fix(sub): bracket ipv6 hosts in share links

* fix(frontend): bracket ipv6 hosts in share links
2026-06-14 23:38:58 +02:00
MHSanaei 41cb0b8ae7 fix(inbounds): show remark first, else inbound tag, in client labels
Revert formatInboundLabel to the pre-#5151 behavior: display the inbound
remark when set, otherwise the inbound tag, instead of "tag (remark)".
Affects the Attach clients / Attached inbounds views and client lists.
Routing keeps its own tag (remark) formatting.
2026-06-12 20:37:37 +02:00
Rouzbeh† 0766e16684 feat: implement inbound XMUX form fields (#5211)
* feat: implement inbound XMUX form fields

* fix: replace any cast to satisfy eslint

* test: update xhttp form snapshot for XMUX

* fix(inbound): persist xmux on save so the XMUX form actually round-trips

The inbound wire normalizer unconditionally deleted xhttpSettings.xmux,
so the new inbound XMUX form was stripped on save and never reached the
stored config — the subscription extra blob (buildXhttpExtra) could
never see it. Gate the deletion on the enableXmux toggle, mirroring the
outbound adapter, and add regression tests for both on/off cases.

* fix(xmux): enforce xray-core's maxConnections/maxConcurrency exclusivity

xray-core's XmuxConfig rejects a config that sets both maxConnections
and maxConcurrency. The panel pre-fills maxConcurrency ('16-32') whenever
XMUX is enabled, so an explicit maxConnections would always collide and
make xray refuse the config. Mirror core's semantics in the wire
normalizer: when maxConnections is set (>0, an explicit opt-in since it
defaults to 0), drop the leftover default maxConcurrency. Applies to both
inbound and outbound xhttp.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-12 12:31:13 +02:00
MHSanaei f1a4286e2f feat(sub): per-inbound sort order for subscription links
Add a subSortIndex field to inbounds that controls the order of links
in subscription output only: the raw sub body, the HTML sub page, and
the JSON/Clash formats (all served from the same query). Lower values
come first; ties keep id order. The panel inbound list is unaffected.

The value is editable in the inbound form next to the share-address
fields, propagates to nodes via wireInbound, and follows the usual
node-sync rules (copied on import, mirrored while not dirty, never a
structural change).

Rescoped from #5214 by @Ponywka.
2026-06-12 12:03:22 +02:00
MHSanaei d04cb10971 feat(wireguard): per-peer comments for identifying devices (#5168)
WG peers were only identifiable by their keys. Add an optional panel-side
comment per peer: editable in the inbound form (echoed next to "Peer N"
in the section header), stored in the settings JSON alongside the
panel-only privateKey (xray-core ignores unknown peer fields), and
appended to the share link / .conf remark so the device is identifiable
in client apps too.
2026-06-12 09:10:57 +02:00
MHSanaei bade1fcef6 feat(ui): allow custom fragment packets ranges, not just presets (#5075)
The fragment "packets" field was a locked dropdown (tlshello / 1-3 / 1-5)
in both the finalmask TCP-mask form and the Freedom outbound form, while
xray-core accepts any "n-m" packet range. Replace both with an
AutoComplete that keeps the presets as suggestions and validates free
input as "tlshello" or a numeric range.
2026-06-12 09:04:17 +02:00
MHSanaei 60da6bed15 fix(xhttp): stop injecting scMaxEachPostBytes/scMinPostsIntervalMs defaults (#5141)
The panel seeded xhttp configs with scMaxEachPostBytes=1000000 and
scMinPostsIntervalMs=30 — xray-core''s own defaults — and emitted them
into every generated config and share link. The literal
scMinPostsIntervalMs=30 is a stable DPI fingerprint that Russia''s TSPU
keys on to block connections on mobile networks.

New configs no longer seed these values (empty schema/template defaults,
so xray-core applies its internal defaults). For configs already stored
with the old defaults, the link/subscription builders now drop values
equal to xray-core''s defaults instead of advertising them — covering
panel share links, the raw subscription, and the JSON subscription
without requiring every inbound to be re-saved. Non-default values the
user set deliberately are still emitted.
2026-06-12 01:50:37 +02:00
iYuan 2a7342baa9 feat: add inbound share address strategy (#5162)
* feat: add inbound share address strategy

Allow node-managed inbounds to choose whether exported share links use the node address, routable listen address, or a custom endpoint. Preserve locally configured share address fields during remote node traffic sync.

Refs #5161

Refs #4891

* fix: preserve inbound share address settings

Forward share address fields to remote nodes, keep existing values when older update payloads omit them, align localhost handling between frontend and subscriptions, and preserve share address settings when cloning inbounds.

* fix: keep share address strategy out of subscriptions

Limit the new share address strategy to direct exported share links and QR codes. Restore subscription address resolution to the existing panel-owned behavior and update the UI help text accordingly.

* fix: address share address review feedback

* fix: validate custom share address

* fix

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 20:24:15 +02:00
aleskxyz 8f408d2d6a feat(routing): show tag (remark) in routing rules list (#5151)
* feat(routing): show tag (remark) in routing rules list

Rules table and mobile cards showed raw inboundTag while the form already
used remarks. Display "tag (remark)" when a remark exists; saved rules
still store tags only.

Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com>

* feat(inbounds): show "tag (remark)" consistently wherever an inbound is listed

Add a shared formatInboundLabel/formatInboundTag helper and apply the "tag (remark)" format across the routing rules table, mobile cards, the rule form and route tester, plus the client attach/detach/filter modals and the attached-inbounds column. Falls back to the bare tag when no distinct remark exists.

Also fix the routing rules list mis-rendering inbounds whose remark contains a comma: formatted entries are now carried as an array end to end instead of being joined and re-split on commas.

---------

Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 12:46:24 +02:00
nima1024m 941eba546d feat(clients): restore traffic usage progress bars on Clients page (#5150)
Bring back the v2.9.x traffic column UX: used amount, color-coded progress bar, limit/infinity label, and hover popover with upload/download/remaining breakdown. Adds a shared ClientTrafficCell component, traffic display helpers, and unit tests.
2026-06-11 12:10:49 +02:00
Rouzbeh† c7a76e9626 fix: enable XTLS vision flow for VLESS+XHTTP+vlessenc in UI and share links (#5157) (#5185)
* fix: enable XTLS vision flow for VLESS+XHTTP+vlessenc in UI and share links (#5157)

* fix: enable xtls-rprx-vision flow for VLESS XHTTP with vlessenc encryption (#5157)

The flow selector was hidden and the vless:// link omitted flow= because:
1. The backend gate (inboundCanEnableTlsFlow) only accepted tcp+tls/reality.
2. The PR #5185 frontend check used `encryption === 'vlessenc'`, which never
   matches — the stored value is a generated ML-KEM dotted string, not the CLI
   subcommand name.

Fix: extend inboundCanEnableTlsFlow to also return true for XHTTP when a
non-none vlessenc encryption/decryption value is present. Update all three
call-sites (inbound.go TlsFlowCapable field, client_crud.go clientWithInboundFlow,
inbound_clients.go copy-flow path) and the sub/service.go link generator.
Scope is XHTTP-only: TCP without tls/reality is intentionally excluded.

Add inbound_protocol_test.go covering the new and existing gate combinations,
extend client_flow_isolation_test.go with xhttp+vlessenc cases, and add
frontend tests for canEnableTlsFlow with real ML-KEM key values.

---------

Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 12:04:02 +02:00
Rouzbeh† 1ad483ede6 fix: expose streamSettings for Tunnel inbounds to support TProxy (#5171)
* fix: expose streamSettings for Tunnel inbounds to support TProxy

* fix(ui): hide security tab for tunnel inbounds when stream is enabled

tunnel (dokodemo-door) does not support TLS or Reality, so showing the
security tab only results in a fully-disabled radio group. Exclude tunnel
alongside wireguard from the security tab.

* fix(tunnel): restrict stream tab to sockopt-only and fix transportless schema

Tunnel (dokodemo-door) only needs sockopt.tproxy for TProxy mode — no
user-selectable transport. Add hasSelectableTransport flag to hide the
network picker, per-network sub-forms, ExternalProxy, and FinalMask for
both tunnel and wireguard, matching the pattern already used for Hysteria.

Fix a pre-existing Zod schema bug where NetworkSettingsSchema was a bare
discriminatedUnion requiring `network` to be present. Wireguard and
tunnel submit streamSettings without a `network` key, causing
"Invalid discriminator value. Expected 'tcp' | ..." on every save. Fix
by adding a transportless union branch (z.never().optional()) alongside
the transport DU; also add ?? 'tcp' fallback in inbound-link.ts where
stream.network is now string | undefined. Three regression tests added.

---------

Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-06-11 11:05:42 +02:00
Rouzbeh† 4002be4ade feat: support latest Wireguard features from Xray-core (PRs #5643, #5833, #5850) (#5131)
* feat: support latest Wireguard features from Xray-core

Implements support for Xray-core PRs #5833, #5643, and #5850 for Wireguard Inbounds:
- Adds 'domainStrategy' and 'workers' to Wireguard inbound configuration.
- Enables the Stream Settings tab for Wireguard inbounds to configure 'sockopt' and 'finalmask', hiding the irrelevant 'network' transmission dropdown.
- Adds the 'randRange' field to the 'noise' UDP Finalmask obfuscation settings.

* fix

---------

Co-authored-by: Rqzbeh <Rqzbeh@example.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-10 17:02:41 +02:00
Rouzbeh† fe62c39a53 fix: inbound edit validation failure and legacy copy to clipboard (#5132)
* fix: auto-enable clients when resetting traffic

When a client's traffic is exhausted, the panel automatically disables the client and pushes enable: false to the nodes. However, when an admin clicked 'Reset Traffic' or used bulk reset, the counters were zeroed but the client was left disabled. This forced administrators to manually re-enable the client across the central panel and remote nodes.

This patch updates ResetTrafficByEmail and BulkResetTraffic to automatically set Enable: true for any previously disabled client and push the updated settings to nodes, ensuring the client is instantly restored upon traffic reset.

* fix: inbound edit validation failure and legacy copy to clipboard
2026-06-09 15:55:55 +02:00
MHSanaei 6c1594693d feat(mtproto): add domain-fronting and essential mtg options
Expose mtg's [domain-fronting] section (ip/port/proxy-protocol) plus
proxy-protocol-listener, prefer-ip, and debug on MTProto inbounds. Each
key is written to the generated mtg-<id>.toml only when set, so mtg's own
defaults apply otherwise. The instance fingerprint now covers these
fields, so editing an option restarts the sidecar.

Since MTProto is mtg-served (not Xray), sniffing does not apply: hide the
Sniffing tab and the Advanced sniffing sub-editor, drop it from the
Advanced "All" JSON view, and emit empty sniffing in the wire payload,
all gated by a new canEnableSniffing predicate.
2026-06-09 12:44:04 +02:00
Sanaei f8e89cc848 fix(mtproto): reap orphaned mtg, fix SysLog viewer, mtg log visibility, export remark (#5105) (#5107)
* fix(logs): render journalctl output in the SysLog viewer

The log viewer's parseLogLine only understood the app-log format
(2006/01/02 15:04:05 LEVEL - body). With SysLog ticked the backend
returns journalctl lines (Mon DD HH:MM:SS host ident[pid]: LEVEL - body),
so the parser mistook the journal time for the level and dropped the
body, leaving only timestamps. Detect and strip the journald prefix,
keep the journal timestamp as the stamp, then parse the real level and
body from the remainder.

* feat(mtproto): surface mtg output and add status reporting

mtg's stdout/stderr was captured by a writer that kept only the last
line and showed it nowhere, so the reason a proxy could not reach
Telegram was invisible. Stream mtg output line-by-line into the x-ui
log, tagged per inbound, so it appears in the panel log viewer and
journald.

Also fix mangled log lines: logger.Info uses fmt.Sprint, which drops
the space between adjacent string operands, producing output like
'inbound3on0.0.0.0:8443'. Switch the affected mtproto calls to the
formatted (*f) variants.

Add show_mtproto_status to x-ui.sh so 'x-ui status' reports each
mtproto inbound's mtg process state and bind address.

* fix(logs): parse all journalctl message shapes in SysLog viewer

Real journalctl output mixes four message shapes after the
'Mon DD HH:MM:SS host ident[pid]:' prefix: go-logging 'LEVEL - msg'
(x-ui/xray), Go std-log with an embedded date (net/http, runtime),
telego's '[timestamp] LEVEL msg', and systemd lines. The viewer only
understood the first, so std-log and telego lines — which never contain
' - ' — collapsed to a bare timestamp (e.g. the 8s telego 409 spam).

Extract the parser into a pure, testable module and teach it the other
shapes: strip the redundant Go std-log date, lift the level out of
telego brackets, and always keep the message body. Add a unit test
covering each shape with real captured lines.

* fix(mtproto): reap orphaned mtg sidecars so a stale one can't break new clients

On Linux x-ui does not kill its mtg children when it dies (no kill-on-exit,
unlike the Windows job object). After a crash, OOM, kill -9, or update, a
stale mtg keeps holding the inbound port with an OLD secret, so new clients
fail the FakeTLS handshake and get silently domain-fronted to the fakeTLS
domain instead of proxied to Telegram (a few MB of traffic, never connects).

Sweep orphans at startup: on the first reconcile, before x-ui starts any of
its own mtg, scan /proc and SIGKILL any process whose executable is our
mtg-<goos>-<goarch> binary. x-ui is the sole owner of mtg, so anything alive
then is an orphan. Runs once per process (swept guard), survives the
binary-deleted-during-update case via /proc/<pid>/cmdline, and is a no-op on
Windows (job object) and other platforms.

Also clear stray mtg in update.sh/install.sh after stopping x-ui, anchored to
the 'mtg-linux-<arch> run ' invocation so the pattern can't match unrelated
command lines (e.g. x-ui.sh's own 'grep mtg-linux').

* fix(logs): drop dead body initializer flagged by eslint no-useless-assignment

* fix(mtproto): drop remark fragment from tg://proxy export link

The mtproto export link appended the inbound remark as a URL fragment
(tg://proxy?server=...&port=...&secret=...#remark). Telegram Desktop
rejects a proxy deep link with a trailing fragment as 'This proxy link
is invalid', breaking one-click import, and a remark is meaningless for
proxy links across clients. Stop adding it in both the panel link
(genMtprotoLink) and the subscription service. Fixes #5105.

* fix(x-ui.sh): remove unused check_mtproto_status helper

show_mtproto_status does its own process check, so check_mtproto_status
was dead code. Drop it (per Copilot review on #5107).
2026-06-09 04:01:33 +02:00
Sanaei 1ca5924a44 feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar (#5076)
* feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar

Xray-core has no mtproto proxy, so mtproto inbounds run as standalone
mtg (9seconds/mtg) sidecar processes managed by the panel — one per
inbound — and are excluded from the generated Xray config entirely.

- model: MTProto protocol constant, validator, and FakeTLS secret
  helpers (GenerateFakeTLSSecret/HealMtprotoSecret)
- mtproto package: per-inbound mtg process manager with reconcile,
  graceful stop, and best-effort Prometheus traffic scraping
- runtime: delegate mtproto inbounds to the mtg manager instead of the
  Xray gRPC API; skip mtproto when building the Xray config
- web: boot reconcile + StopAll wiring, periodic reconcile/traffic job,
  port-conflict transport, secret healing on inbound add/update
- sub: tg:// proxy share-link generation
- frontend: protocol option, Zod schema, Protocol tab (FakeTLS domain +
  regenerable secret), info-modal link, and i18n
- provisioning: fetch mtg v2.2.8 in install.sh, DockerInit.sh, and the
  Linux + Windows release workflows

* fix

* fix

* fix: address Copilot review comments on mtproto PR

- web/web.go: create NewMtprotoJob once and reuse for cron + initial run
- mtproto/manager.go: StopAll cleans up per-inbound config files on shutdown
- mtproto/manager.go: CollectTraffic releases mutex before HTTP scrapes to
  avoid blocking Ensure/Reconcile/Remove during network I/O
- database/model/model.go: panic on crypto/rand failure in mtprotoRandomMiddle
  instead of silently producing a weak all-zero secret
- install.sh: fix chmod to handle renamed bin/mtg-linux-arm on armv5/v6/v7
2026-06-08 14:28:19 +02:00
Sanaei af3c808444 fix: default hysteria tls to no utls fingerprint 2026-06-08 13:15:51 +02:00
MHSanaei 483952cfa0 fix(finalmask): validate fragment mask length so empty/zero-min can't crash xray
A fragment TCP finalmask with an empty length (the form's default for a
newly added mask) serializes to a 0-0 range, and xray-core rejects
LengthMin == 0 with a fatal config error that aborts the whole process,
taking every inbound offline. Default a new fragment mask to length
100-200 and add a form validator rejecting an empty value or a zero
minimum range before save. Verified against xray 26.6.1 (#4998).
2026-06-06 13:34:53 +02:00
nima1024m 6ed6f57b5c fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target (#4988)
* fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target

Strip mode-specific XHTTP fields for stream-one, reset harmful sockopt defaults
to 0, split server/client Reality fields on save, validate target host:port in
the inbound form, and expose Happy Eyeballs for the direct freedom outbound.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(panel): keep REALITY public key on the wire, guard freedom noises

The REALITY server/client wire split deleted realitySettings.settings on save, but the panel stores the REALITY public key there and every share-link / subscription generator reads it back from that path (frontend inbound-link.ts, Go subService/subJsonService/subClashService). Stripping it produced empty pbk= links, breaking client connectivity after save+reload.

Revert the reality normalization (drop normalizeRealityForWire and the key sets), restore the inbound REALITY form fields (uTLS, spiderX, publicKey, mldsa65Verify) while keeping the new validated target field, and restore the mldsa65Verify clear handler.

Also guard freedomToWire against undefined noises/finalRules (same defensive treatment as the existing fragment guard, issue #4686) which the new freedom-outbound test surfaced as a crash. Tests now assert the public key is preserved.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-06-06 02:40:32 +02:00
MHSanaei 5b9db13e55 fix(finalmask): treat sudoku customTables as array of tables
customTables is the plural array form of customTable, so default it to [""] and edit it with a tags Select instead of binding a text Input to an array value.
2026-06-06 01:35:14 +02:00
MHSanaei e7ffae5329 fix(outbound): import ech and pcs from TLS share links
The vless/trojan link parser's TLS branch read only sni/fp/alpn, so the
ech (echConfigList) and pcs (pinnedPeerCertSha256) query params were
dropped on import even though buildStream allocates both fields. Read
them in applySecurityParams to match the inbound link generator and the
hysteria2 parser.
2026-06-05 11:01:51 +02:00
MHSanaei a8d5d0dfab fix(external-proxy): relabel "Host" as "Address", add per-entry ECH (#4935)
The external proxy "Host" field was bound to dest (the connection address that becomes the link host) but labeled "Host", misleading users into thinking it set a transport host header. Relabel it to "Address" to match what it actually controls.

Add per-entry ECH (echConfigList) to the external proxy schema, form (shown under Force TLS = TLS), the TS link generator, and the Go sub services: ech is emitted on share links and vmess objects, and written into the stream so the JSON subscription picks it up via the existing tlsData reader.
2026-06-05 10:40:11 +02:00
MHSanaei f8e902a7b6 fix(sub): include ECH config in TLS share links and JSON subscription
echConfigList was stored under tlsSettings.settings but the share-link
and JSON-subscription generators only read fingerprint and
pinnedPeerCertSha256 from that bag, silently dropping ECH from VLESS,
Trojan and VMess links. Read echConfigList alongside them and flatten it
into tlsSettings.echConfigList for the JSON subscription.

Closes #4933
2026-06-05 00:20:29 +02:00
biohazardous-man 97f88fb1a9 feat(sub): modern xray JSON format with unified finalmask editor (#4912)
* feat(sub): add finalmask support to JSON subscriptions

* feat(sub): modern xray JSON format with unified finalmask editor

Drop the legacy JSON subscription format entirely and always emit the
modern xray shape:

- Flatten proxy outbounds (no vnext/servers) for vless/vmess/trojan/
  shadowsocks; hysteria was already flat.
- Express fragment/noise via streamSettings.finalmask instead of the
  legacy direct_out freedom dialer + dialerProxy sockopt.

The global finalmask (tcp/udp masks + quicParams) is stored as a single
setting (subJsonFinalMask) and merged into every generated stream,
replacing the separate subJsonFragment/subJsonNoises/subJsonQuicParams
settings.

Reuse the existing FinalMaskForm (used by inbound/outbound) for the
settings UI via a small bridge component; add a showAll prop so all
TCP/UDP/QUIC sections render for the global case. This supersedes the
hand-rolled Fragment/Noises/quicParams tabs with the full mask editor
(all mask types).

Note: this is a breaking change — JSON subscriptions now require a
recent xray client on the consumer side.

* fix

---------

Co-authored-by: biohazardous-man <biohazardous-man@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-06-04 23:51:48 +02:00
MHSanaei db5ce06256 fix(panel-proxy): route custom geo and http(s) Telegram through panelProxy
Custom geosite/geoip downloads built their own ssrfSafeTransport and never used the configured Panel Network Proxy, so geo updates failed on servers where GitHub is filtered. Route all custom-geo HTTP (startup probes + downloads) through panelProxy when set, falling back to the direct SSRF-guarded transport otherwise; the target URL stays SSRF-validated.

The Telegram bot only honored a socks5:// panel proxy and silently rejected http(s)://, despite the setting advertising both. Branch the fasthttp dialer (FasthttpHTTPDialer for http(s), FasthttpSocksDialer for socks5) and accept all three schemes in the fallback and NewBot validation.

Add tests proving the panel proxy is used by custom geo and that the bot dialer speaks HTTP CONNECT vs SOCKS5 per scheme.
2026-06-03 14:57:49 +02:00
MHSanaei e7c11c913a feat(inbounds): per-proxy Pinned Peer Cert SHA-256 + labeled External Proxy form
Redesign the Add Inbound -> Stream External Proxy section into labeled per-entry cards (Force TLS / Host / Port / Remark and, under TLS, SNI / Fingerprint / ALPN) and add a Pinned Peer Cert SHA-256 field with a generate-random-hash button to each entry.

The pin flows end to end into share links: pcs for vmess/vless/trojan/ss (stripped when a proxy forces security off) and the hex-normalized pinSHA256 for Hysteria. JSON and Clash subscriptions emit the native pinnedPeerCertSha256 / pin-sha256 via the cloned stream. Adds the forceTls label across all 13 locales plus frontend and Go tests.
2026-06-03 13:46:54 +02:00
MHSanaei d0998c1d6d feat(links): richer share-link labels across QR, client info and sub views
Show colored protocol/transport/security tags followed by the inbound remark and port for each share link in the client QR modal, client info modal and subscription page. The client email and the traffic/expiry decorations are stripped from the remark so only the inbound remark and port remain.

Consolidate the duplicated per-page parseLinkMeta/trimEmail/PROTOCOL_COLORS into a shared lib/xray/link-label.tsx (parseLinkParts, LinkTags, linkMetaText) so the colours and the email/stats stripping stay identical across all three surfaces.
2026-06-03 02:18:40 +02:00
MHSanaei 6ee462ac8e fix(links): use configured domain for panel copy/QR links on loopback
The panel's copy/QR share links are built client-side and fell back to window.location.hostname, so reaching the panel over an SSH tunnel (127.0.0.1/localhost) leaked localhost into the links - unlike the backend subscription path, which falls back to the configured Sub/Web Domain (issue #4829).

Expose webDomain/subDomain via /defaultSettings and add preferPublicHost: when the browser host is loopback, prefer the configured Sub Domain (then Web Domain) for share/QR links. An explicit node override or per-inbound listen still wins; a routable browser host is kept as-is.

Closes #4829
2026-06-02 22:52:44 +02:00
MHSanaei ac67c52278 fix(hysteria2): emit pinSHA256 as hex in subscriptions, not base64
Hysteria2 clients backed by Xray-core hex-decode the pinSHA256 URI param and crash on the base64 value the panel stores for pinnedPeerCertSha256 (xray-core native TLS format). Normalize each pin to bare lowercase hex when building the Hysteria link, accepting base64, bare hex, and colon-separated openssl fingerprints; values that are neither are passed through untouched. Applied in both the backend subscription generator and the frontend link builder. The pcs share-link and JSON-sub paths keep base64 for their consumers. Fixes #4818.
2026-06-02 18:52:26 +02:00
MHSanaei 13d02f01fc feat(hysteria2): emit UDP port hopping in subscriptions and share links
UDP Hop (finalmask.quicParams.udpHop.ports) was configurable but never surfaced in generated configs, so clients kept using the single listening port (#4789).

Share links (frontend genHysteriaLink + sub genHysteriaLink) now keep a numeric port in the authority and carry the hop range as the v2rayN-compatible mport query param, so v2rayN and other System.Uri-based importers can parse the link. Clash output sets mihomos native ports field.

Closes #4789
2026-06-02 15:01:18 +02:00
MHSanaei b2e2120eb3 feat(inbounds): support Unix domain socket path in Listen field (#4429)
UDS listen already worked for proxying (the listen string is passed to xray verbatim and port 0 is accepted), and the Go sub/link layer already ignores the bind listen. The only gap was the frontend resolveAddr, which would put a socket-path listen into share/sub links (e.g. vless://uuid@/run/xray/x.sock:0). resolveAddr now treats a path-style listen (starting with / or @) as having no client-reachable address and falls back to hostOverride/hostname. Adds a test and a Listen-field help hint across all locales.
2026-06-02 00:37:20 +02:00
MHSanaei 588ea86298 fix(hysteria): use pinSHA256 for pinned cert and emit ech in share links
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.
2026-06-01 22:02:37 +02:00
MHSanaei 803e010921 fix(outbound): carry ALPN, fingerprint and UDP mask when importing a Hysteria2 link (#4760)
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.
2026-06-01 19:21:29 +02:00
MHSanaei 13c04bb982 fix(outbound): fill encryption and pqv when importing VLESS link
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.
2026-06-01 17:37:54 +02:00
MHSanaei 2bb9ed1cda feat(outbound): sync DNS outbound config with Xray core changes
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.
2026-06-01 10:24:35 +02:00
MHSanaei 32f96298f8 feat(finalmask): sync transport with upstream Xray core changes
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.
2026-06-01 10:12:51 +02:00
MHSanaei a3dca4b82d fix(inbounds): drop listen address from auto-generated inbound tag
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.
2026-06-01 09:33:49 +02:00
MHSanaei 971843f669 feat(nodes): bulk panel self-update with live online indicator
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.
2026-06-01 07:03:06 +02:00
MHSanaei eb78b8666f fix(inbound): re-derive auto tags on edit and keep node tags consistent
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.
2026-06-01 05:08:29 +02:00
MHSanaei f02018cfb7 fix(outbounds): prevent freedom save crash, complete its fields (#4686)
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
2026-05-31 19:50:50 +02:00
Sanaei d1882c7f29 refactor(frontend): reorganize source tree & break down oversized modals/tabs (#4698)
* 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).
2026-05-30 21:51:33 +02:00
MHSanaei 12afb862ff fix(outbounds): parse wireguard:// links and fix ss:// query-string port
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.
2026-05-29 21:27:50 +02:00
MHSanaei 8c30ddbfd9 fix(outbounds): persist optional blocks and fix stale edit reopen
- 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
2026-05-29 19:10:31 +02:00