mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
6f05c0a49252d2e4d2aa4e0d77dc3b070e5fe28e
2884 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ffde2f7ebf |
feat(sub): add Copy All Configs button to subscription page (#5163)
* feat(sub): add Copy All Configs button to subscription page * fix(sub): include links in copyAll dependency array Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * chore: fmt * fix(sub): drop module-level links from copyAll deps to satisfy exhaustive-deps links is derived from window.__SUB_PAGE_DATA__ at module scope, so listing it in the useCallback dependency array triggers a react-hooks/exhaustive-deps warning (outer-scope value). Matches the existing single-link copy callback's deps. --------- Co-authored-by: nikan <nikan> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
89b1137b00 |
feat(env): allow setting the initial URI path for the web panel (#5149)
* feat(env): allow setting the initial URI path for the web panel * fix(setting): normalize and guard XUI_INIT_WEB_BASE_PATH default Address Copilot review on PR #5149: an env value that is empty, whitespace, or lacks slashes (e.g. `panel`) could produce an invalid webBasePath such as `/ /` and reach the frontend un-normalized. getEnv now trims whitespace and falls back when the value is empty; the env-derived default is passed through the existing normalizeBasePath helper (reused from node.go) so it always carries a leading and trailing slash. GetBasePath reuses the same helper instead of duplicating the slash logic. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
eee652c4a5 |
chore(deps): bump golang.org/x/net from 0.55.0 to 0.56.0 (#5199)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.55.0 to 0.56.0. - [Commits](https://github.com/golang/net/compare/v0.55.0...v0.56.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-version: 0.56.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
57e9661758 |
fix: properly configure fail2ban backend and dependencies on Ubuntu 22.04+ (#5159) (#5184)
Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> |
||
|
|
65fa40b819 |
fix: accurately retrieve and generate API tokens via CLI with hashed storage (#5145) (#5183)
Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> |
||
|
|
f88f53cd7b |
fix(update): restart panel after regenerating webBasePath to fix login desync
When update.sh regenerates a short webBasePath, it writes the new path to the database after the panel is already running with the old path loaded in memory. Without a restart the server keeps serving the old path while the UI shows the new one, making the new path unreachable. |
||
|
|
ca4f32e3da |
feat: replace panel proxy URL with outbound-based egress bridge
Instead of requiring a manual SOCKS5/HTTP URL, the panel now lets the admin pick an Xray outbound from a dropdown (same UX as Geodata Auto-Update). At runtime, injectPanelEgress appends a loopback SOCKS inbound (tag: panel-egress) and prepends a routing rule so the panel's own HTTP traffic — version checks, Telegram, normal geo-file updates — is routed through the chosen outbound. Xray-native Geodata Auto-Update is unaffected (it uses its own geodata.outbound inside Xray). Blackhole outbounds are excluded from both picker dropdowns since routing any download through one just drops it. Translations updated for all 13 locales. |
||
|
|
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 |
||
|
|
3092326d9e |
refactor: replace custom geo manager with Xray-core native geodata auto-update
Remove the panel-side custom geo download feature (service, controller, /panel/api/custom-geo/* endpoints, CustomGeoResource model, UI tab) in favor of Xray-core's native geodata section (https://xtls.github.io/config/geodata.html). - pass the top-level "geodata" key through xray.Config so it survives the template round-trip into the generated config - add a Geodata Auto-Update section to the Xray Updates modal that edits geodata (cron schedule, download outbound, asset list) in the config template and restarts Xray on save - previously downloaded geo files in the bin folder keep working in ext: routing rules; the orphaned custom_geo_resources table is left in place so existing source URLs stay recoverable |
||
|
|
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> |
||
|
|
f9b275dd23 |
fix(ui): keep client IP log modal above edit modal (#5137)
* fix: keep client IP log modal above edit modal * refactor: name client modal z-index values |
||
|
|
dbb269cf6a |
fix(ui): correct inline style syntax between clients count and active clients count on inbounds page (#5114)
* fix(ui): correct inline style syntax in client counts column on inbounds page * fix(ui): correct inline style syntax between clients count and active clients count on inbounds page |
||
|
|
d047075f76 | docs: add Turkish language link to other README files (#5138) | ||
|
|
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.
|
||
|
|
26c549a95a |
fix(client): match clients by email for delete/update, not credentials
Delete/update located the client in an inbound's settings JSON by the record's credential (uuid/password/auth). When that credential drifted from the inbound JSON -- e.g. a rotated UUID left behind, or duplicated by a past partial-update bug -- the lookup failed with "Client Not Found In Inbound For ID: <uuid>" and aborted the whole operation, making the client impossible to remove from the panel. Key every delete/update/detach path on email, the client's stable identity. This survives credential drift and heals duplicate-email entries by removing all of them. - Delete/DeleteByEmail/Detach/DetachByEmailMany -> DelInboundClientByEmail - delInboundClients / bulkDelInboundClients: match settings by email - UpdateInboundClient: locate the entry to replace by email (param clientId -> oldEmail); update all callers to pass the email - bulkAdjustInboundClients: match by email - writeBackClientSubID: pass email; drop unused sourceProtocol param - make per-inbound deletion idempotent via ErrClientNotInInbound - remove now-orphaned DelInboundClient, clientKeyForProtocol and getClientPrimaryKey; scale test deletes by email |
||
|
|
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 |
||
|
|
2969f6e91d | fix(client): preserve UUID/password/auth on partial client update (#5111) | ||
|
|
0bed552292 |
fix(outbound): include tested outbound in HTTP probe config (#5120)
HTTP-pinging a subscription outbound always reported "Probe timed out". The frontend sends only the template outbounds as allOutbounds, but subscription outbounds are injected at runtime and aren't in that list, so burstObservatory had no outbound matching the tag to probe. Append the tested outbound when its tag is missing instead of only when allOutbounds is empty, so the probe always has a target while preserving the template outbounds that back dialerProxy chains. |
||
|
|
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. |
||
|
|
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).v3.3.0 |
||
|
|
9711a9ce22 | v3.3.0 | ||
|
|
9acde8da9d |
Bump frontend version and deps
Update frontend package.json and refresh dependencies for a new release (frontend version -> 0.3.0). Regenerated lockfile and upgraded multiple JS packages (notably @swagger-api/apidom family, @rc-component packages, codemirror, etc.) and added libc metadata where applicable. Also update Go module dependencies (go.mod and go.sum) as part of routine dependency maintenance. |
||
|
|
d9ccf157c3 |
feat: add manual and automatic WARP IP rotation (#5099)
* feat: add manual and automatic WARP IP rotation * fix: update generated api and frontend schemas * fix(warp): validate rotation interval, fix auto-update timing, sync editor - Validate the auto-update interval as an integer and store it via setInt; a non-integer value previously broke GetAllSetting for the whole panel. - Seed warpLastUpdate when the interval is saved and when changing IP manually, so auto-update counts from "now" instead of epoch 0 and a manual rotation doesn't trigger an immediate scheduled one. - Guard WarpIpJob: when lastUpdate is unset, establish a baseline and skip instead of rotating on the next tick. - Log WARP license re-apply failures instead of swallowing them. - After a manual "Change IP", sync the in-memory Xray editor with the keys the backend persisted so a later template save can't revert them; only toast success when the interval save actually succeeds. - Add the WARP rotation UI strings to all 13 locales. - Drop trailing whitespace introduced in entity.go and xray_setting.go. --------- Co-authored-by: Rqzbeh <Rqzbeh@example.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
be8bd4e22c |
fix: propagate inbound traffic reset to nodes (#5103)
Co-authored-by: Rqzbeh <Rqzbeh@example.com> |
||
|
|
5a7de02598 | fix(ui): remove pointer cursor from non-interactive elements in cards (#5102) | ||
|
|
a32c6803da |
fix: route WARP API requests through panel proxy (#5101)
Co-authored-by: Rqzbeh <Rqzbeh@example.com> |
||
|
|
9f31d7d056 |
feat: synchronize access.log client IPs across nodes (#5098)
* feat: synchronize access.log client IPs across nodes for global fail2ban limits * fix(nodes): harden cross-node client-IP merge for cluster fail2ban MergeInboundClientIps inserted new rows with the remote node's primary key, which collides with the independently auto-incremented local id and rolled back the whole sync batch — breaking exactly the node-only clients the feature targets. It also never evicted stale IPs, so the 30-minute cutoff was defeated cluster-wide (the master pushed its unpruned table back to nodes, which re-added IPs they had just pruned) and the blobs grew unbounded. - drop the remote id on create (Id=0) and guard the email-unique race with ON CONFLICT DO NOTHING; also fixes a latent Postgres sequence collision - apply the same 30-minute stale cutoff inside the merge and skip creating node-only rows whose IPs are all stale - throttle the IP fetch/merge/push to ~10s (data only refreshes every 10s) instead of running on every 5s traffic tick, cutting SQLite write churn - log the load error on the push path and tidy the merge response message - add unit tests for the merge (remote-id, dedup, stale-drop, skips) --------- Co-authored-by: Rqzbeh <Rqzbeh@example.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
0d7b6872f7 |
docs(i18n): refine Turkish translation and network terminology (#5092)
* Add files via upload * Delete README.md * Add files via upload * Delete README_EN.md * docs(i18n): refine Turkish translation and network terminology |
||
|
|
7d908834a8 | fix(ui): correct inline style syntax in client counts column on inbounds page (#5097) | ||
|
|
1fa51cf0f2 |
feat(groups): show used traffic per group in groups table
Sum up+down across each group's clients via a LEFT JOIN on email in ListGroups, expose it as trafficUsed on GroupSummary, and render it as a new column plus a "Total traffic" summary card. Drops the unused "Empty groups" card and its translation key. |
||
|
|
b24b8524b6 |
fix(inbounds): drop unknown nodeId when importing an inbound
Importing an inbound that was exported from another panel (or whose node was later deleted) carried a non-zero nodeId referencing a node that does not exist on the importing panel. AddInbound -> nodePushPlan -> NodeSyncState looked that node up and returned gorm's "record not found", so the import failed with "Something went wrong Failed: record not found". Node IDs are panel-local and not portable, so the import handler now drops a nodeId that does not exist on this panel (new NodeService.NodeExists helper), importing it as a local inbound instead of erroring. This mirrors the existing nodeId==0 normalization in the same handler. |
||
|
|
8ce61f3cb0 |
fix(script): revoke also removes cert files and acme.sh tracking (#5009)
The SSL Certificate Management "Revoke" option only called acme.sh --revoke, leaving local files under /root/cert/<domain>, acme.sh renewal state under ~/.acme.sh/<domain>, and the panel cert paths in the DB. Renamed to "Revoke & Remove": it now also drops acme.sh tracking, deletes the local cert directory and acme.sh state dirs (RSA + ECC), resolves the real IP address(es) for IP certs so their renewal state is torn down too, and clears the panel cert paths (x-ui cert -reset) + restarts when the deleted domain was in use. |
||
|
|
3d6ff2b60c |
fix(tgbot): apply bot settings on panel restart without full service restart
The Telegram bot's config (enable flag, token, proxy, API server) was read only during a full server.Start(). The in-process "Restart Panel" path (StopPanelOnly/StartPanelOnly -> stop/start with the bot flag false) skipped the bot entirely, so enabling the bot or changing its proxy did not take effect until a full OS-level `systemctl restart x-ui`. Cycle the Telegram bot in the panel-only restart path while still leaving Xray and the traffic writer untouched (preserving the #4265 freeze fix), so "Restart Panel" reconciles the bot with the latest settings. Fixes #5033 |
||
|
|
abf6b8799e |
feat: customizable subscription page templates (#5079)
* feat: add support for subscription-based outbounds with auto-update
- New OutboundSubscription model (full support on both SQLite and PostgreSQL)
- Go subscription link parser (vmess/vless/trojan/ss/hysteria2/wireguard) matching frontend behavior
- Stable tag assignment across refreshes (designed for balancer + routing use)
- Runtime merge of subscription outbounds into Xray config (additive only)
- Full CRUD + manual refresh + preview API
- Background auto-update job (per-subscription interval)
- Frontend management UI in Outbounds tab (Subscriptions drawer) + tag integration in balancers/routing rules
- Proper dual-database support including CLI migration path
Review & hardening notes:
- Fixed merge logic bug that could drop manual outbounds
- Added SSRF/private-IP protection on subscription URLs using SanitizePublicHTTPURL
- Improved update interval UX (hours + minutes)
- Auto-fetch on first subscription creation
- Added detailed comments on tag stability strategy and balancer implications when servers are added/removed/rotated
- Updated migrationModels() for CLI migrate-db support
* fix: resolve frontend lint/type errors and Go build break
Frontend (eslint + tsc clean):
- Destructure subscriptionOutboundTags prop in RoutingTab and
BalancersTab. It was declared in the interface and used in useMemo
but never destructured, so it resolved as an unresolved global
(react-hooks warning + tsc "Cannot find name"). The prop is passed
by XrayPage, so the feature was silently inert.
- OutboundsTab: remove unused useEffect import, add an OutboundSub
type to replace any[] state and the any/any table render signature,
type the subscriptionOutbounds cast, and replace unused catch (e)
bindings with parameter-less catch. Also type HttpUtil.post as
OutboundSub so r.obj?.id type-checks.
Backend (go build clean):
- outbound_subscription_job: websocket.MessageTypeXray is undefined;
use the existing MessageTypeOutbounds since the job refreshes
outbound subscriptions.
* fix(xray): make outbound subscription creation work end-to-end
- Correct API paths from /panel/xray/outbound-subs to
/panel/api/xray/outbound-subs. The controller is mounted under
/panel/api, so the old paths hit the SPA page route (GET-only)
and 404'd on POST.
- Send the create-subscription body as a plain object instead of
URLSearchParams. The axios request interceptor serializes bodies
with qs.stringify, which can't read URLSearchParams' internal
storage and produced an empty body, so the backend rejected it
with "subscription URL is required".
- Use message.useMessage() + context holder instead of the static
antd message API (resolves the "Static function can not consume
context" warning), matching XrayPage's pattern.
- Migrate the subscriptions Drawer to antd v6 props: width -> size,
destroyOnClose -> destroyOnHidden, and Space direction -> orientation.
* feat(xray): show traffic/test for subscription outbounds; harden + test the feature
Display (the reported issue):
- Replace the flat read-only pills with a proper read-only table (desktop)
and cards (mobile) in a new SubscriptionOutbounds component, showing
Address, Protocol, Traffic (matched by tag — already collected by Xray),
and a Test button with Latency. No edit/delete/move (read-only).
- Test subscription outbounds via the existing /testOutbound endpoint, with
results keyed by tag (subscriptionTestStates + testSubscriptionOutbound in
useXraySetting, wired through XrayPage). Generalize isTesting/testResult to
a string|number key so the same helpers serve index- and tag-keyed states.
i18n:
- Replace all hardcoded English subscription strings with t() calls and add
pages.xray.outboundSub.* keys to en-US.json (other locales fall back).
Backend hardening + tests:
- xray.go: drop the tautological `subSvc != nil` check.
- outbound_subscription: re-validate every redirect hop against private/
internal addresses (CheckRedirect) and cap the redirect chain, closing an
SSRF gap where only the initial host was checked.
- Extract assignStableTags as a pure function and add unit tests for tag
stability and SSRF rejection (the feature previously had no tests).
Misc:
- gofmt util/link/outbound.go (it was not gofmt-clean).
* fix(xray): make outbound-subs feature pass CI (test compile, route docs, openapi)
- outbound_test.go: remove unused `inner`/`lines` variables that broke the
`util/link` test build (declared and not used).
- Document the 7 outbound-subscription routes in endpoints.ts (list, create,
update, delete, del alias, refresh, parse) so TestAPIRoutesDocumented passes.
- Regenerate frontend/public/openapi.json (npm run gen) to include the new
endpoints, satisfying the codegen freshness check.
* feat(xray): per-subscription allow-private, gap-filled tags, UI tweaks, delete refresh
Backend:
- Add a per-subscription AllowPrivate flag (default off). Create/Update/refresh
and the redirect check sanitize the URL with it, so localhost/LAN sources work
only when explicitly opted in; the SSRF guard still blocks private targets by
default. Controller reads the allowPrivate form field on create/update/parse.
- Default outbound tag prefix now uses the smallest free "subN-" number instead
of the auto-increment id, so deleting a subscription frees its number for reuse
(a fresh start gives sub1) while staying stable per subscription. Extracted a
pure defaultPrefixNumber() with unit tests.
- deleteOutboundSub now signals SetToNeedRestart so xray drops the outbounds.
Frontend:
- "Allow private address" toggle in the add form (sends allowPrivate).
- Delete now refreshes the xray view immediately (no manual page reload).
- Subscriptions manager opens as a centered Modal instead of a right-side Drawer.
- Move Outbounds to a top-level sidebar item under Nodes (out of Xray Configs).
- Collapse WARP/NordVPN into a "more" dropdown.
- Document the allowPrivate param in endpoints.ts.
* i18n(xray): translate outbound-subscription UI into all locales
- Translate the pages.xray.outboundSub.* strings (and allowPrivate label/hint)
into all 12 non-English locales, matching each file's existing terminology.
- Remove the unused outboundSub.add ("Add subscription") key from every locale.
* feat: add custom subscription page template support
Allow panel admins to use a custom HTML template for the subscription
page instead of the default React-based SPA.
Changes
-------
Backend
- web/service/setting.go: Add subThemeDir setting (default: empty)
with a getter GetSubThemeDir().
- web/entity/entity.go: Add SubThemeDir field to AllSetting.
- sub/subController.go: In serveSubPage, before falling back to the
embedded SPA, check if subThemeDir is set and the directory exists.
Look for sub.html first, then index.html. Parse with Go html/template
and execute, injecting all standard page variables as template context.
On any parse/execute error, log and fall through to the default page.
Two backward-compat aliases added to the template data map:
- result = links (for tx-ui v2 templates using {{ range .result }})
- jsonUrl = subJsonUrl
Frontend
- frontend/src/models/setting.ts: Add subThemeDir = '' to AllSetting.
- frontend/src/pages/settings/SubscriptionGeneralTab.tsx: Add a Sub
Theme Directory input in Subscription settings.
Templates
- sub_templates/README.md: Full authoring guide with all variables.
- sub_templates/tx-ui/index.html: The tx-ui subscription page template
migrated from v2 to v3 data shape.
Credits
-------
Bundled tx-ui template from AghayeCoder: https://github.com/AghayeCoder/tx-ui
* chore: regenerate OpenAPI schemas and types for custom sub-template feature
* feat(xray): subscription manager — edit, reorder/priority, status, preview, refresh-all
Backend:
- Per-subscription Priority + Prepend: subscriptions are ordered by Priority and
placed before (Prepend) or after the manual template outbounds in the merge, so
a subscription server can become the default. New Move(up/down) endpoint
re-normalizes priorities; merge split into prepend/template/append.
- List now returns a derived OutboundCount and orders by priority, and strips the
heavy LastFetchedOutbounds/LinkIdentities blobs from the list payload.
- Create/Update accept the prepend flag; new subs append at the end of priority.
Frontend (Outbound Subscriptions modal):
- Edit existing subscriptions (reuses the form + Update endpoint).
- Inline enable/disable Switch, Status column (OK / error tooltip), Outbounds
count column, per-row refresh spinner, "Refresh all" button.
- Reorder (move up/down) controls + a "Before manual outbounds" toggle.
- Preview button: fetch+parse a URL via /parse without saving.
- Document the move route + prepend param in endpoints.ts; regenerate openapi.json.
* i18n(xray): translate new subscription-manager strings into all locales
Add the prepend/prependHint, preview/previewEmpty, refreshAll, statusOk and
toastUpdated keys to all 12 non-English locales, matching each file's terminology.
* refactor(sub): harden custom template rendering, drop bundled tx-ui template
Builds on the custom subscription page template feature.
Rendering hardening (sub/subController.go):
- Render the custom template into a buffer and only write the response on
success. Previously template.Execute wrote straight to the ResponseWriter,
so a mid-render failure left a partially-written body and then fell through
to the default page, corrupting the response (superfluous WriteHeader).
- Cache parsed templates keyed by path, invalidated by file mtime, so each
subscription page load no longer re-reads and re-parses the file from disk;
admin edits are still picked up automatically.
- Verify the configured path is a directory (IsDir) and log a Warning when it
is set but unusable / an Error when a template fails to parse, instead of
silently falling back.
- Expose two new template variables: subTitle and subSupportUrl.
Cleanup:
- Remove the bundled tx-ui template and all tx-ui / AghayeCoder references
(including the result/jsonUrl v2-compat aliases); use a generic my-theme
example path in docs/UI/translation.
- i18n the "Sub Theme Directory" setting (en-US subThemeDir/subThemeDirDesc)
instead of hardcoded English.
- Fix README: expire is seconds (not ms), lastOnline is ms; correct the
settings tab name; note templates are admin-provided, not bundled/deployed.
Tests:
- Add sub/subController_test.go covering loadSubTemplate: render, sub.html
precedence, fallback cases, malformed template, and mtime cache invalidation.
Verified end-to-end in Docker: custom template renders with all variables,
all fallback paths return the clean default page (no corruption), and the
mtime cache reflects live edits.
* i18n(settings): translate subThemeDir into all locales
Add the subThemeDir / subThemeDirDesc keys (Sub Theme Directory setting) to
all 12 non-English locales, matching each file's existing terminology. They
previously fell back to en-US.
---------
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com>
|
||
|
|
94b8196e84 |
fix(db): additional cross-DB and node traffic edge cases (migration scan + node reset time) (#5045)
* fix(db): additional cross-DB and node traffic edge cases
- ExternalProxy migration: change StreamSettings scan from []byte
to string (text column on both SQLite and Postgres). Use []byte()
for json.Unmarshal. Avoids potential scan/encoding differences
in migration of old multi-domain data on PG.
- Node mirror inbound creation and updates in SetRemoteTraffic:
now copy LastTrafficResetTime from the node's reported snapshot.
Previously, resets done on the node (or via API that affects
node-owned inbounds) would not update the grace period tracking
on the central mirror. This improves traffic reset + node traffic
combining accuracy when using the public API to manage node
inbounds or when nodes perform resets.
These are independent additional issues around node traffic
combining, creating mirrored node inbounds from snapshots,
and migration code that can affect Postgres (or mixed) setups
after API changes or node operations. They do not depend on the
previous enable-merge or tag/sub fixes.
Base: upstream/main (separate PR).
* fix(db): even more node/SQLite edge cases (chunked IN for node stats, tag cleanup)
- In NodeService.GetAll (used for node list/stats): the load of client_traffics
for node inbound IDs used a direct "IN ?" with all IDs. On SQLite this
can hit the bind var limit ("too many SQL variables") when there are
many nodes/inbounds. Chunked using the existing chunkInts + sqliteMaxVars
(same pattern as other large IN queries in the package). This is a
specific scale issue for "node" setups on SQLite (PG is fine with large IN).
- Tag cleanup raw in MigrationRequirements (always runs at startup):
was using SQLite-only INSTR. Fixed to use position() on PG (same as
the previous tag fix on the main branch). Prevents startup crash on PG
after node/inbound API changes that leave old tags.
These are additional specific cases around node traffic/stats combining,
node inbound counts, and startup migrations that can affect Postgres
users or large SQLite node deployments. They are independent of the
enable/traffic core fixes and the prior additional ones.
Added to the clean additional-issues branch for the separate PR.
* fix(db): even more for node traffic merge on 5045 (dialect enable expr + chunk gone deletes)
- Full dialect-safe client enable merge in setRemoteTrafficLocked:
- Added ClientTrafficEnableMergeExpr() helper (PG CASE with ::boolean
casts to avoid type errors; SQLite numeric for affinity).
- Updated GreatestExpr with ::bigint casts on PG.
- Switched the merge UPDATE from "enable AND ?" to the helper.
This completes the node traffic sync safety for the "only node can
disable" logic across DBs (core of the original symptom after API
inbound updates on nodes).
- Chunked the NodeClientTraffic delete for "goneEmails" (when a node's
snapshot no longer includes clients previously attached to a mirrored
inbound). The "email IN ?" could exceed SQLite bind limit for nodes
with many clients (after API deletes, bulk ops, or structural changes).
Uses chunkStrings + sqliteMaxVars (consistent with the node stats chunk
we added earlier).
These are direct extensions of node traffic combining, mirrored inbound
lifecycle, and API-driven changes that affect client_traffics / NodeClientTraffic
for nodes. Stayed on the clean 5045 branch as requested.
Pushed to update https://github.com/MHSanaei/3x-ui/pull/5045
---------
Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com>
|
||
|
|
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> |
||
|
|
1c74b995c3 |
feat(nodes): add distinct purple indicator when panel is online but Xray core failed (#5040)
* feat(nodes): add distinct purple indicator when panel is online but Xray core failed Currently nodes only show binary online/offline based on panel API reachability. This adds a third state: - Green: panel reachable + Xray healthy - Purple pulsing dot + "Online (Xray Error)": panel API works (management actions still available) but the node Xray process is in error or stopped. Tooltip shows the remote xrayError. - Red: unreachable (unchanged) Backend now captures xray.state + xray.errorMsg from /panel/api/server/status heartbeats and probes. New fields on Node + NodeSummary, forwarded for transitive nodes. Frontend Zod + NodeList rendering + dedicated .xray-error-dot CSS (color #722ED1) + i18n key. Color chosen purple per feedback after initial implementation. Refs: worktree xray-failed-in-nodes * fix: remove invalid JSON comment causing CI failures * chore: regenerate OpenAPI schemas and types for xray error indicators * chore: regenerate examples and schemas for xray error indicators * chore: regenerate missing openapi.json examples * fix --------- Co-authored-by: Rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
0daedd3db9 |
feat: add support for subscription-based outbounds with auto-update (#5037)
* feat: add support for subscription-based outbounds with auto-update
- New OutboundSubscription model (full support on both SQLite and PostgreSQL)
- Go subscription link parser (vmess/vless/trojan/ss/hysteria2/wireguard) matching frontend behavior
- Stable tag assignment across refreshes (designed for balancer + routing use)
- Runtime merge of subscription outbounds into Xray config (additive only)
- Full CRUD + manual refresh + preview API
- Background auto-update job (per-subscription interval)
- Frontend management UI in Outbounds tab (Subscriptions drawer) + tag integration in balancers/routing rules
- Proper dual-database support including CLI migration path
Review & hardening notes:
- Fixed merge logic bug that could drop manual outbounds
- Added SSRF/private-IP protection on subscription URLs using SanitizePublicHTTPURL
- Improved update interval UX (hours + minutes)
- Auto-fetch on first subscription creation
- Added detailed comments on tag stability strategy and balancer implications when servers are added/removed/rotated
- Updated migrationModels() for CLI migrate-db support
* fix: resolve frontend lint/type errors and Go build break
Frontend (eslint + tsc clean):
- Destructure subscriptionOutboundTags prop in RoutingTab and
BalancersTab. It was declared in the interface and used in useMemo
but never destructured, so it resolved as an unresolved global
(react-hooks warning + tsc "Cannot find name"). The prop is passed
by XrayPage, so the feature was silently inert.
- OutboundsTab: remove unused useEffect import, add an OutboundSub
type to replace any[] state and the any/any table render signature,
type the subscriptionOutbounds cast, and replace unused catch (e)
bindings with parameter-less catch. Also type HttpUtil.post as
OutboundSub so r.obj?.id type-checks.
Backend (go build clean):
- outbound_subscription_job: websocket.MessageTypeXray is undefined;
use the existing MessageTypeOutbounds since the job refreshes
outbound subscriptions.
* fix(xray): make outbound subscription creation work end-to-end
- Correct API paths from /panel/xray/outbound-subs to
/panel/api/xray/outbound-subs. The controller is mounted under
/panel/api, so the old paths hit the SPA page route (GET-only)
and 404'd on POST.
- Send the create-subscription body as a plain object instead of
URLSearchParams. The axios request interceptor serializes bodies
with qs.stringify, which can't read URLSearchParams' internal
storage and produced an empty body, so the backend rejected it
with "subscription URL is required".
- Use message.useMessage() + context holder instead of the static
antd message API (resolves the "Static function can not consume
context" warning), matching XrayPage's pattern.
- Migrate the subscriptions Drawer to antd v6 props: width -> size,
destroyOnClose -> destroyOnHidden, and Space direction -> orientation.
* feat(xray): show traffic/test for subscription outbounds; harden + test the feature
Display (the reported issue):
- Replace the flat read-only pills with a proper read-only table (desktop)
and cards (mobile) in a new SubscriptionOutbounds component, showing
Address, Protocol, Traffic (matched by tag — already collected by Xray),
and a Test button with Latency. No edit/delete/move (read-only).
- Test subscription outbounds via the existing /testOutbound endpoint, with
results keyed by tag (subscriptionTestStates + testSubscriptionOutbound in
useXraySetting, wired through XrayPage). Generalize isTesting/testResult to
a string|number key so the same helpers serve index- and tag-keyed states.
i18n:
- Replace all hardcoded English subscription strings with t() calls and add
pages.xray.outboundSub.* keys to en-US.json (other locales fall back).
Backend hardening + tests:
- xray.go: drop the tautological `subSvc != nil` check.
- outbound_subscription: re-validate every redirect hop against private/
internal addresses (CheckRedirect) and cap the redirect chain, closing an
SSRF gap where only the initial host was checked.
- Extract assignStableTags as a pure function and add unit tests for tag
stability and SSRF rejection (the feature previously had no tests).
Misc:
- gofmt util/link/outbound.go (it was not gofmt-clean).
* fix(xray): make outbound-subs feature pass CI (test compile, route docs, openapi)
- outbound_test.go: remove unused `inner`/`lines` variables that broke the
`util/link` test build (declared and not used).
- Document the 7 outbound-subscription routes in endpoints.ts (list, create,
update, delete, del alias, refresh, parse) so TestAPIRoutesDocumented passes.
- Regenerate frontend/public/openapi.json (npm run gen) to include the new
endpoints, satisfying the codegen freshness check.
* feat(xray): per-subscription allow-private, gap-filled tags, UI tweaks, delete refresh
Backend:
- Add a per-subscription AllowPrivate flag (default off). Create/Update/refresh
and the redirect check sanitize the URL with it, so localhost/LAN sources work
only when explicitly opted in; the SSRF guard still blocks private targets by
default. Controller reads the allowPrivate form field on create/update/parse.
- Default outbound tag prefix now uses the smallest free "subN-" number instead
of the auto-increment id, so deleting a subscription frees its number for reuse
(a fresh start gives sub1) while staying stable per subscription. Extracted a
pure defaultPrefixNumber() with unit tests.
- deleteOutboundSub now signals SetToNeedRestart so xray drops the outbounds.
Frontend:
- "Allow private address" toggle in the add form (sends allowPrivate).
- Delete now refreshes the xray view immediately (no manual page reload).
- Subscriptions manager opens as a centered Modal instead of a right-side Drawer.
- Move Outbounds to a top-level sidebar item under Nodes (out of Xray Configs).
- Collapse WARP/NordVPN into a "more" dropdown.
- Document the allowPrivate param in endpoints.ts.
* i18n(xray): translate outbound-subscription UI into all locales
- Translate the pages.xray.outboundSub.* strings (and allowPrivate label/hint)
into all 12 non-English locales, matching each file's existing terminology.
- Remove the unused outboundSub.add ("Add subscription") key from every locale.
* feat(xray): subscription manager — edit, reorder/priority, status, preview, refresh-all
Backend:
- Per-subscription Priority + Prepend: subscriptions are ordered by Priority and
placed before (Prepend) or after the manual template outbounds in the merge, so
a subscription server can become the default. New Move(up/down) endpoint
re-normalizes priorities; merge split into prepend/template/append.
- List now returns a derived OutboundCount and orders by priority, and strips the
heavy LastFetchedOutbounds/LinkIdentities blobs from the list payload.
- Create/Update accept the prepend flag; new subs append at the end of priority.
Frontend (Outbound Subscriptions modal):
- Edit existing subscriptions (reuses the form + Update endpoint).
- Inline enable/disable Switch, Status column (OK / error tooltip), Outbounds
count column, per-row refresh spinner, "Refresh all" button.
- Reorder (move up/down) controls + a "Before manual outbounds" toggle.
- Preview button: fetch+parse a URL via /parse without saving.
- Document the move route + prepend param in endpoints.ts; regenerate openapi.json.
* i18n(xray): translate new subscription-manager strings into all locales
Add the prepend/prependHint, preview/previewEmpty, refreshAll, statusOk and
toastUpdated keys to all 12 non-English locales, matching each file's terminology.
---------
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
|
||
|
|
21e01cc1e6 |
fix(postgres): make node traffic sync robust after public API inbound updates (#5038)
* fix(postgres): make node traffic sync robust after public API inbound updates
The background NodeTrafficSyncJob (every 5s) started failing after a
successful POST /panel/api/inbounds/update/{id} (including flows that
inject streamSettings.externalProxy) with:
node traffic sync: merge for <node> failed:
ERROR: CASE types boolean and integer cannot be matched (SQLSTATE 42804)
Root cause:
- The merge lives in setRemoteTrafficLocked (called from SetRemoteTraffic).
- The client_traffics delta path used a dialect-sensitive expression:
enable = enable AND ?
last_online = GREATEST(last_online, ?)
- On PostgreSQL, GREATEST / AND / COALESCE are implemented with internal
CASE expressions. When "enable" columns (client_traffics, inbounds, ...)
were INTEGER (common after SQLite → PG data migrations, older
AutoMigrate, or mixed write paths) and the right-hand side was a
boolean parameter (from snapshot ClientStats or form-bound API payload),
PG rejected the expression at plan time.
- The public API update path (unlike the internal remote wire path)
always runs updateClientTraffics + UpdateClientStat + SyncInbound.
This touches client_traffics.enable rows for any inbound that has
clients.
- SQLite tolerated 0/1 numeric bools; PG is strict.
Fix:
- Use an explicit CASE with ::boolean casts in the critical enable
expression so the result type is always boolean.
- Make GreatestExpr emit safe casts on Postgres.
- Add a one-time normalization step in MigrationRequirements (runs on
startup + xray restarts) that forces the relevant enable/enabled
columns to boolean on Postgres using an idempotent DO block + USING
cast. This cleans up pre-existing skew without a full re-migration.
This branch is based on upstream/main (original mhsanaei/3x-ui main).
The node traffic sync now survives arbitrary public-API inbound
updates on PostgreSQL.
* fix: make client traffic enable merge expression safe on SQLite too
The previous commit introduced an explicit CASE for the "only node
can disable" logic in the node traffic sync merge to fix the PG
"CASE types boolean and integer cannot be matched" error after
public API inbound updates.
That expression used PostgreSQL-only `::boolean` casts:
CASE WHEN ?::boolean THEN enable::boolean ELSE false END
This is invalid syntax on SQLite (and would break the merge when
the client_traffics delta UPDATE runs — which is commonly triggered
right after an API /inbounds/update because that path calls
updateClientTraffics + SyncInbound and touches client_traffics rows).
Extracted the expression to a new dialect-aware helper
`ClientTrafficEnableMergeExpr()` (following the same pattern as
GreatestExpr, JSONClientsFromInbound, etc.).
- On Postgres: keeps the strict boolean-typed CASE with casts.
- On SQLite: uses a numeric-compatible form
`CASE WHEN ? THEN enable ELSE 0 END` that produces the expected
0/1 result matching the column affinity.
The logical behavior ("node may only force-disable, never re-enable")
is preserved on both databases.
This is a follow-up commit on the same branch so that one PR
contains both the original Postgres fix and the SQLite compatibility
fix.
Builds directly on top of
|
||
|
|
46684dd164 |
fix(sub): emit VLESS encryption in Clash configs (#5053)
Co-authored-by: jq <fs187438@gmail.com> |
||
|
|
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 |
||
|
|
af3c808444 | fix: default hysteria tls to no utls fingerprint | ||
|
|
98ba88037c | fix(subClashService): improve merging of clash rules in YAML (#5054) | ||
|
|
d739bcf71e | fix arm architecture xray binary file name (#5060) | ||
|
|
b0fe21c804 |
i18n(tr): Improve Turkish translation consistency and terminology (#5066)
Thank you for this great project! I've made a comprehensive revision of the Turkish translation to improve consistency, grammatical accuracy, and natural flow for Turkish-speaking network administrators. **Key Improvements:** - **Unified "Client" Terminology:** Consistently translated as "Kullanıcı" (User) for human accounts and "İstemci" (Client) for software applications throughout the UI and Telegram Bot. - **Inbounds & Outbounds:** Replaced the literal translations with professional networking terms: "Bağlantı Noktaları" (Inbounds) and "Çıkış Noktaları" (Outbounds). - **Vowel Harmony Fixes:** Corrected several Turkish grammatical vowel harmony issues (e.g., *kullanıcısi* → *kullanıcısı*, *kullanıcılarini* → *kullanıcılarını*). - **Capitalization & Phrasing:** Fixed capitalization inconsistencies (e.g., "Son Çevrimiçi") and improved phrasing for terms like "camouflage" → "Maskeleme" and "transport" → "Aktarım". Technical English terms (SNI, TLS, REALITY, grpc, Vision, etc.) are intentionally kept in English as they are the standard in network engineering. Hope this helps the Turkish community! |
||
|
|
f6558571b4 |
docs(i18n): Add Turkish translation for README (#5067)
Hello! I noticed there was no Turkish README file despite having many other languages. I have created README.tr_TR.md to help the Turkish community properly understand and deploy 3x-ui. The translation uses highly accurate networking terminology that matches the recent tr-TR.json improvements. Thank you! |