Commit Graph

51 Commits

Author SHA1 Message Date
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
Nikan Zeyaei f4bbaf40f0 feat(ui): show per-inbound live speed (#5261)
* feat(utils): add speedFormat utility and tests

* feat(inbounds): add InboundSpeedEntry type

* feat(inbounds): add speed column to inbound list

* feat(inbounds): show speed in inbound stats modal

* feat(inbounds): compute inbound speed from traffic deltas

* feat(inbounds): wire inbound speed through page

* feat(i18n): add speed translation for all locales

* refactor(inbounds): dedupe live-speed UI and harden formatting

Extract a shared InboundSpeedTag component and isActiveSpeed guard used by the speed column and stats modal, unify InboundSpeedEntry into a single type, and route speedFormat through sizeFormat.

Also guard sizeFormat against non-finite input (no more "NaN PB/s") and clear stale per-inbound speeds when a traffic poll returns no deltas.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-14 22:39:40 +02:00
iYuan 90e6217749 fix(inbound): preserve custom share strategy on edit (#5225) 2026-06-12 18:38:01 +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 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
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
MHSanaei 6b16d8c37a feat: apply inbound/outbound/routing changes live via Xray gRPC API
Add a hot-apply layer that computes a diff between the old and new
generated config and applies only the changed parts through the Xray
gRPC HandlerService and RoutingService, avoiding a full process restart
whenever possible. A restart is still performed when sections that have
no reload API (log, dns, policy, observatory, ...) actually change.

Key additions:
- internal/xray/hot_diff.go: ComputeHotDiff with canonical-JSON
  comparison (sorted keys, null=absent, full number precision) so UI
  reformatting never triggers a spurious restart
- internal/xray/api.go: AddOutbound/DelOutbound, ApplyRoutingConfig,
  GetBalancerInfo, SetBalancerTarget, TestRoute gRPC wrappers
- internal/web/service/xray.go: tryHotApply, ensureAPIServices,
  GetBalancersStatus, OverrideBalancer, TestRoute service methods
- internal/web/controller/xray_setting.go: balancerStatus,
  balancerOverride, routeTest API endpoints
- frontend: BalancersTab live-status/override columns, RouteTester
  component, Restart button removed (Save now hot-applies)
- balancer-helpers.ts: syncObservatories never creates observatory
  sections for random/roundRobin balancers (no reload API → restart)
- i18n: balancerLive/Override/routeTester keys added to all 13 locales
2026-06-10 23:01:33 +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
Sanaei 41645255f1 refactor: focused service files, leaf subpackages, and an internal/ layout (#5167)
* refactor(service): split client.go into focused files

client.go had grown to 4455 lines mixing ~10 responsibilities. Split it
verbatim into cohesive same-package files (no behavior change):

  client.go            foundation: ClientService, ClientWithAttachments,
                       ClientCreatePayload, ErrClientNotInInbound, sqlInChunk
  client_locks.go      inbound mutation locks, delete tombstones, compactOrphans
  client_lookup.go     read-only lookups (GetByID, List, EffectiveFlow, ...)
  client_link.go       inbound association sync (SyncInbound, DetachInbound, ...)
  client_crud.go       single-client CRUD + validation + protocol defaults
  client_inbound_apply.go  low-level inbound-settings mutators + by-email setters
  client_bulk.go       bulk attach/detach/adjust/delete/create + DelDepleted
  client_traffic.go    traffic-reset paths
  client_groups.go     client group management
  client_paging.go     paged listing, filtering, sorting, summary

Every declaration moved unchanged (verified: identical func/type/const/var
signature set before vs after). Imports redistributed per file via goimports.
go build ./..., go vet, and go test ./web/service/... all pass.

* refactor(service): split inbound.go into focused files

inbound.go was 4100 lines. Split it verbatim into cohesive same-package
files (no behavior change):

  inbound.go             core inbound CRUD + InboundService (keeps pkg doc)
  inbound_protocol.go    protocol / stream capability helpers
  inbound_node.go        node/runtime/remote coordination + online tracking
  inbound_traffic.go     traffic accounting, reset, client stats
  inbound_client_ips.go  per-client IP tracking
  inbound_clients.go     client lookups within inbounds + copy-clients
  inbound_disable.go     auto-disable invalid inbounds/clients
  inbound_migration.go   DB migrations
  inbound_sublink.go     subscription link providers
  inbound_util.go        generic slice/string helpers

Identical func/type/const/var signature set before vs after; package doc
comment preserved on inbound.go. Imports redistributed via goimports.
Build, vet, and go test ./web/service/... all pass.

* refactor(service): split tgbot.go into focused files

tgbot.go was 3738 lines dominated by a 1246-line answerCallback. Split it
verbatim into cohesive same-package files (no behavior change):

  tgbot.go           lifecycle, bot setup, caches, small utils
  tgbot_router.go    incoming update / command / callback dispatch
  tgbot_send.go      outbound messaging primitives
  tgbot_client.go    client views, actions, subscription links
  tgbot_inbound.go   inbound listing / pickers
  tgbot_report.go    server usage, exhausted, online, backups, notifications

Identical func/type/const/var signature set before vs after. Imports
redistributed via goimports. Build, vet, and go test ./web/service/... pass.

* refactor(client): dedupe single-field by-email setters

ResetClientIpLimitByEmail, ResetClientExpiryTimeByEmail, and
ResetClientTrafficLimitByEmail shared an identical ~50-line body that
resolves the inbound by email, confirms the client exists, rewrites a
single-client settings payload, and delegates to UpdateInboundClient.

Extract that into applyClientFieldByEmail(inboundSvc, email, mutate) and
reduce each setter to a 3-line wrapper. Behavior is unchanged: same checks
and error strings, same single-client payload contract, same totalGB guard.

SetClientTelegramUserID (resolves by traffic id, different error text) and
ToggleClientEnableByEmail/SetClientEnableByEmail (different return shape and
a pre-read of the old state) intentionally keep their own bodies.

* refactor(service): extract panel/ subpackage

Move the panel-administration leaf services out of the flat service
package into web/service/panel/ (package panel):

  user.go         UserService (auth / 2FA / LDAP)
  panel.go        PanelService (restart / self-update) + version helpers
  panel_other.go  non-unix RestartPanel
  panel_unix.go   unix RestartPanel
  api_token.go    ApiTokenService
  websocket.go    WebSocketService
  panel_test.go   version/shellQuote unit tests

These are leaves: they depend on core (SettingService, Release) but no
core file references them, so the extraction creates no import cycle.
Core references are now qualified (service.SettingService, service.Release);
callers in main.go, web/web.go, and web/controller/* updated to panel.*.
Build, vet, and go test ./web/... pass.

* refactor(service): extract integration/ subpackage

Move the external-provider integration leaves into web/service/integration/
(package integration):

  warp.go        WarpService (Cloudflare WARP)
  nord.go        NordService (NordVPN)
  custom_geo.go  CustomGeoService (custom geo asset management)
  *_test.go      custom_geo / panel-proxy tests

These depend on core (SettingService, ServerService, XraySettingService) but
no core file references them. xray_setting.go stays in core because it calls
the unexported SettingService.saveSetting. The shared isBlockedIP SSRF helper
(used by core url_safety.go and by custom_geo) now has a small copy in each
package rather than being exported. Core references qualified; callers in
web/web.go, web/job/*, and web/controller/* updated to integration.*.
Build, vet, and go test ./web/... pass.

* refactor(service): extract tgbot/ subpackage

Move the Telegram bot (6 files + test) into web/service/tgbot/ (package
tgbot). It is a leaf: it embeds five core services (Inbound/Client/Setting/
Server/Xray) and the core never references it, so no import cycle.

To support the package boundary without changing behavior:
  - core exposes XrayProcess() *xray.Process so tgbot keeps calling the
    exact same running-process methods it used via the package-level `p`;
  - three core methods tgbot calls are exported: ClientService.checkIs-
    EnabledByEmail -> CheckIsEnabledByEmail, InboundService.getAllEmails ->
    GetAllEmails (callers updated in-package);
  - tgbot's embedded-field types and the few core type refs (Status,
    ClientCreatePayload, SanitizePublicHTTPURL) are now service-qualified.

Callers in main.go, web/web.go, web/job/*, and web/controller/* updated to
tgbot.*. Build, vet, and go test ./web/... pass.

* refactor(service): extract outbound/ subpackage

OutboundService (outbound.go) imports only neutral packages (config,
database, model, xray) and its production code is referenced by no core or
sibling service file — only by web/controller/xray_setting.go and
web/job/xray_traffic_job.go. Move it to web/service/outbound/ (package
outbound); no core qualification needed inside. Callers updated to outbound.*.

The one coupling was a tiny pure test helper, outboundsContainTag, used by
both outbound.go and the core outbound_subscription_test.go; it now has a
small copy in that test file rather than being shared across the boundary.
Build, vet, and go test ./web/... pass.

* refactor(util): move wireguard into its own subpackage

util/wireguard.go was the lone file of the root `util` package (24 lines,
one exported func GenerateWireguardKeypair), while every other util concern
lives in a focused subpackage (util/common, util/crypto, util/netsafe, ...).
Move it to util/wireguard/ (package wireguard) for consistency; its only
importer, web/service/integration/warp.go, is updated. The root `util`
package no longer exists.

* refactor(sub): drop redundant sub prefix from filenames

Inside package sub the subXxx.go prefix just repeats the package name
(like client_*.go did inside service). Rename for consistency; content and
type names are unchanged:

  subController.go    -> controller.go
  subService.go       -> service.go
  subClashService.go  -> clash_service.go
  subJsonService.go   -> json_service.go
  (+ matching _test.go files)

* refactor(controller): rename xui.go -> spa.go

XUIController serves the panel's single-page-app shell; spa.go names that
role plainly (the other controller files are domain-named). File rename only
— the type stays XUIController. api_docs_test.go keys route base paths by
filename, so its "xui.go" case is updated to "spa.go".

* refactor: move backend packages under internal/

Adopt the idiomatic Go application layout: the backend packages now live
under internal/ (a boundary the toolchain enforces), signalling private
implementation instead of a library-style flat root. No runtime behavior
changes — only import paths and a few build/config paths move.

Moved: config, database, logger, mtproto, sub, util, web, xray -> internal/.
main.go stays at the repo root and tools/openapigen stays under tools/ (both
still import internal/* because the internal rule keys off the module root).
The module path github.com/mhsanaei/3x-ui/v3 is unchanged; 149 .go files had
their import prefix rewritten to .../internal/<pkg>.

Couplings the Go compiler can't see, updated to the new layout:
  - frontend i18n imports of web/translation (react.ts, setup.components.ts)
  - vite outDir + eslint/tsconfig ignore globs -> internal/web/dist
  - Dockerfile COPY paths for web/dist and web/translation
  - locale.go os.DirFS("web") disk fallback -> "internal/web"
  - .gitignore and ci.yml go:embed stub for internal/web/dist
  - api_docs_test.go repo-root relative walk (one level deeper)
  - tools/openapigen filesystem package paths; ApiTokenView repointed to the
    web/service/panel subpackage and codegen regenerated (clears a stale
    type the ci.yml codegen check was failing on)

Verified: go build/vet/test (all packages), and frontend typecheck, lint,
vitest (478 tests), and production build into internal/web/dist.

* fix(config): keep test runs from writing logs into the source tree

GetLogFolder() returns a CWD-relative "./log" on Windows. Under `go test`
the working directory is each package's own folder, so InitLogger (called by
tests in web/job, web/service, xray, web/websocket) created stray log/
directories scattered through the source tree (e.g. internal/web/job/log/).

Redirect to a shared temp folder when testing.Testing() reports a test run.
Production behavior is unchanged: Windows still uses ./log next to the binary
and Linux /var/log/x-ui. The log files were always gitignored (*.log) and
never committed; this just stops the noise at the source.

* docs: move subscription-template guide out of root into docs/

sub_templates/ was a top-level folder holding only a README and no actual
templates (3x-ui ships none by design), referenced nowhere and unlinked from
any doc — it read like an empty placeholder cluttering the repo root.

Move the guide to docs/custom-subscription-templates.md (a proper docs home),
reword its intro to read as documentation rather than a folder note, link it
from the Features list in README.md, and drop the empty sub_templates/ folder.

* fix: update stale web/ path references after the internal/ move

The internal/ migration rewrote Go import paths but left some references to
the old top-level layout in docs, comments, and a few runtime disk paths.

Functional (dev-mode only): the disk-serving fallbacks that read the Vite
build from disk when running from source still pointed at web/dist/, which
moved to internal/web/dist/ — so `os.DirFS`/`os.Stat`/`os.ReadFile` in
internal/web/web.go and internal/sub/{sub,controller}.go are corrected.
Production was unaffected (it serves the embedded FS; verified by the Docker
build), but `go run` with a live frontend build silently fell back to embed.

Docs/comments: frontend/README.md, CONTRIBUTING.md, the claude-issue-bot and
release workflows, the openapigen -root help text, and assorted Go comments
now reference internal/web, internal/database, internal/sub, internal/xray,
etc. Package-name mentions (the "web" package), root paths (main.go,
frontend/, install scripts, /etc/x-ui), routes (/panel/api/xray), and the
historical "web/assets no longer exists" note were intentionally left as-is.

* refactor(web): remove the legacy /xui -> /panel redirect middleware

RedirectMiddleware existed only for backward compatibility with the old
`/xui` URL scheme (301-redirecting /xui and /xui/API to /panel and
/panel/api). That cutover was long ago, so drop the middleware, its
registration in initRouter, and the now-inaccurate "URL redirection"
mention in the middleware package doc. Old /xui URLs now 404 like any other
unknown path. HTTPS auto-redirect and auth redirects are unrelated and stay.

* build: fix .dockerignore for internal/ layout and exclude runtime dir

- web/dist -> internal/web/dist: the embedded frontend moved under internal/,
  so the stale exclude no longer matched and the locally-built dist could be
  sent to the build context (the frontend stage rebuilds it fresh anyway).
- exclude x-ui/: the local runtime directory (SQLite db, geo .dat files, xray
  binaries, certs — ~150MB) was being shipped into the build context for no
  reason. Verified the pattern excludes only the directory and still keeps
  x-ui.sh, which the Dockerfile copies to /usr/bin/x-ui.
2026-06-10 15:19:22 +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
nima1024m e8171ab4f7 fix(xray): sync routing rules when outbound tag is renamed (#5006)
* chore: ignore local .cursor directory

* fix(xray): sync routing rules when outbound tag is renamed

Renaming an outbound in the Outbounds tab only updated the outbound list, leaving routing rules pointing at the old tag. Propagate tag changes to routing rules, balancer selectors, and sockopt dialerProxy references, matching the behavior already used for balancer and WARP/Nord renames.

* test: mock HttpUtil to fix unhandled vitest rejections

* test(frontend): mock axios globally to prevent flaky network errors on CI

* test(frontend): fix eslint any errors in component test setup

---------

Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com>
2026-06-08 20:30:41 +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 a014c01725 feat(api-docs): generate OpenAPI components/schemas from Go structs
A new emit_jsonschema.go walks the same allow-listed structs as the zod/types/examples emitters and writes generated/schemas.ts (SCHEMAS). build-openapi mounts it under components.schemas and points each typed response obj at a $ref instead of an untyped {} blob, so Swagger renders real models and openapi-generator can emit clients.

Also add a vitest guard that safeParses every EXAMPLES entry against its generated zod schema, reviving the previously unused generated/zod.ts and catching drift between the example and schema emitters.
2026-06-06 16:22:21 +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 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 1a64d7e9de feat(tls): add ocspStapling to certificate config
Expose the OCSP Stapling refresh interval (seconds) on the TLS
certificate object in the inbound security form, defaulting to 3600s
to match xray-core. Covers both file-backed and inline cert shapes.
2026-06-03 17:49:36 +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 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 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 b6641439d4 fix(sockopt): rename interfaceName to interface so xray honors it
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.
2026-06-01 18:21:37 +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 48f470c465 fix(test): drain macrotasks via setTimeout, not setImmediate
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.
2026-06-01 09:10:35 +02:00
MHSanaei ed21cf836d fix(test): drain React scheduler macrotask before jsdom teardown
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.
2026-06-01 09:03:47 +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 b67c4c2f81 fix(clients): keep the summary card live without a page refresh
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.
2026-06-01 06:10:25 +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 76dbbfc1f8 feat(inbounds): clearer client validation errors on save
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.
2026-05-31 22:41:58 +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
MHSanaei 62c293e034 fix(outbounds): support proxyProtocol on freedom outbound
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
2026-05-29 17:18:21 +02:00
MHSanaei 3f0b7fbe97 feat(tls): surface pinnedPeerCertSha256 in panel, share links, and subs
Adds a panel-only `pinnedPeerCertSha256` field on TLS settings with a tags input and a random-hash generator. The hashes ride share links as `pcs` (v2rayN-compatible), Clash sub as `pin-sha256`, and JSON sub as `pinnedPeerCertSha256`, while remaining stripped from the run-config sent to xray-core.
2026-05-28 19:32:10 +02:00
Puya c03ecfe638 Fix REALITY share links missing SNI (#4621)
* Fix REALITY share links missing SNI

* Update REALITY link snapshots
2026-05-28 17:11:54 +02:00
MHSanaei 486ac9c28d feat(inbounds): expose Vision testseed field with sensible default
Add a "Vision testseed" form item to the inbound modal for TCP + TLS/reality
inbounds, normalized to positive integers and defaulting to [900,500,900,256].
Apply the same default in the outbound form adapter when no valid saved seed
is present.

Replace the http/mixed snapshot assertions in inbound-defaults with explicit
field checks so generated credentials don't break the snapshots.
2026-05-28 02:33:13 +02:00
Sanaei 272854df91 Client/inbound resilience + Postgres pool tuning + schema fixes (#4607)
* fix(clients): fall back to inbound scan when ClientRecord is missing

DeleteByEmail looked up the email in client_records and returned the
raw "record not found" gorm error when nothing matched, even though
the client could still live inside an inbound's settings.clients JSON
(legacy entries that SyncInbound never picked up, or rows deleted out
from under a stale inbound). The user-visible delete then fails
mysteriously while xray happily keeps serving the client.

When GetRecordByEmail returns ErrRecordNotFound, walk inbounds whose
settings JSON references the email and run DelInboundClientByEmail on
each. The traffic / IP rows are cleaned up at the end unless keepTraffic
is set. If no inbound carries the email either, surface a clear
"client %q not found in any inbound or client record" error instead.

* chore(logging): include request + caller context in jsonMsgObj warnings

The generic "X-UI: Something went wrong. Error: record not found" log
gave no clue about which endpoint, client, or controller line emitted
it. Prepend a context block:

  [POST /panel/api/clients/del/ADMIN ip=109.124.234.127
   handler=controller.(*ClientController).delete client.go:146]

Handler frame is located by scanning the stack for the first caller
outside util.go, so it points at the right controller method whether
the path went through jsonMsg, jsonObj, or jsonMsgObj directly.

* fix(clients): tolerate orphan client_inbounds rows in Delete

DeleteByEmail's previous fix only covered the case where GetRecordByEmail
returned ErrRecordNotFound. When the ClientRecord exists but a client_inbounds
row points to an inbound that has been removed out-of-band (failed mid-delete,
manual SQL, pre-SyncInbound migration), Delete bubbled the raw gorm
"record not found" from inboundSvc.GetInbound and aborted before any cleanup
ran — leaving the client un-deletable through the UI/API.

Match the tolerance bulkDelInboundClients already has: when GetInbound
returns gorm.ErrRecordNotFound for a join row, log a warning and continue.
The unconditional Delete(&model.ClientInbound{}) later in the function then
removes the stale row, and the ClientRecord delete succeeds.

* fix(schemas): accept empty-string fingerprint on externalProxy

The External Proxy form offers a "Default" option with value '' for the
uTLS fingerprint dropdown, but UtlsFingerprintSchema.optional() rejects
empty strings (only undefined or a valid enum member). Saving an inbound
with externalProxy rows failed with `expected one of "360"|"chrome"|...`.

Preprocess '' to undefined before the optional enum, matching the existing
pattern used for VmessSecuritySchema.

* chore(logging): drop noisy orphan client_inbounds warning

Per-row WARNINGs spammed logs whenever a client referenced multiple
already-deleted inbounds. The continue keeps the orphan-tolerant
behavior; just no longer announces each skipped row.

* feat(clients): per-client VMess security in client form

Restores the VMess `security` selector on the client form (auto, aes-128-gcm,
chacha20-poly1305, none, zero) and surfaces it only when at least one attached
inbound is VMess. The value rides into the share link via the existing
`scy=` field in genVmessLink; the panel persists it on ClientRecord and in
the inbound's settings.clients so the link generator can read it back.

Adds the pages.clients.vmessSecurity i18n key in en-US and fa-IR.

* fix(xray-config): strip panel-only fields from inbound config

Two fields the panel stores but Xray doesn't accept on the inbound side:

- VMess clients[].security — panel persists it so the share-link generator
  can write `scy=...`, but xray's vmess inbound spec has no per-client
  security. The field was leaking into the inbound JSON pushed to xray-core.
- VLESS settings.encryption — per the xray spec the inbound only takes
  `decryption`; `encryption` is for the matching client outbound. The panel
  keeps it for operator reference, but it must not appear in the inbound
  payload.

Add two strip helpers next to HealShadowsocksClientMethods and wire them
into GenXrayInboundConfig via a per-protocol switch, so both local and
remote runtime paths get the cleaned config.

* chore(db): backend-aware pool sizes with env overrides

Per-backend defaults:
- Postgres: 25 max open / 25 max idle. Matching idle to open removes
  pool churn under bursts (Postgres handles concurrency at the server,
  idle connections are cheap).
- SQLite: 1 max open / 1 max idle. Single-writer model means a wider
  cap just queues behind busy_timeout; tight cap is honest.

Both back ends share ConnMaxLifetime=1h and ConnMaxIdleTime=30m so
stale connections (vault rotation, pgbouncer drops, load-balancer
idle eviction) rotate out without operator intervention.

Operators can override either default at boot via:
  XUI_DB_MAX_OPEN_CONNS=...
  XUI_DB_MAX_IDLE_CONNS=...

envInt parses these; missing/empty/non-positive values fall back to
the per-backend default.

* fix(schemas): accept boolean acceptProxyProtocol on TCP stream

TcpStreamSettingsSchema declared `acceptProxyProtocol: z.literal(true).optional()`,
so saving an inbound where the AntD Switch sat in the off state failed
validation with `Invalid input` because the Switch always emits a plain
boolean.

Switch to `z.boolean().default(false)` — same shape ws/sockopt/httpupgrade
already use, and matches the actual wire payload (golden fixtures and
other settings blocks all store `acceptProxyProtocol: false`).

Snapshots for stream.test and inbound-full.test pick up the new defaulted
field on TCP fixtures.
2026-05-27 22:51:37 +02:00