mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-18 04:37:14 +07:00
f8db7f6c29e5e2325369bee9b9667a6c5b926f7b
33
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ed5465d0f2 |
feat(clients): support setting HWID limit and MTProto ad-tag in bulk adjust (#6399)
* feat(clients): support setting HWID limit and MTProto ad-tag in bulk adjust Add HWID device limit and Telegram MTProto sponsor channel (ad-tag) support to the bulk client adjustment flow in both the panel API and frontend ClientBulkAdjustModal. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(clients): gate adTag to MTProto inbounds and avoid inbound rewrite for limitHwid Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(clients): stamp updated_at only on the clients a bulk adjust changed The updated_at write was gated on hasInboundChanges, which accumulates over the whole inbound instead of describing the client in hand. Once any client in the settings array changed, every client after it was re-stamped as well, so whether an untouched client kept its own updated_at depended on its position in the array. That field feeds node-snapshot conflict resolution, where a spurious bump lets a stale snapshot value win over the stored record. Track the change per client and fold it into the inbound-level flag where the stamp is written, so the early return still skips a save whose settings JSON would be unchanged. Also condenses the BulkAdjust doc comment back to the two-line maximum. * docs(api): regenerate the bulkAdjust reference for limitHwid and adTag frontend/public/openapi.json was copied to docs/public/, but pnpm gen:api was never re-run, so the API reference page's heading, anchor id and search index still described bulkAdjust without limitHwid or adTag. docs-ci.yml fires only on docs/**, and that path had been touched, so nothing flagged the stale MDX. The externalLinks hunks are the generator rewrapping lines main had left stale, not a content change. * fix(i18n): stop enumerating fields in the bulk-adjust empty-form message bulkAdjustNothing listed the fields the form accepts, so it went stale every time one was added: only en-US ever gained "flow", leaving the other twelve locales describing days and traffic alone, and limitHwid and adTag would have repeated that. Say that one field is required instead of naming which, so the message cannot drift again. |
||
|
|
d5ab84e8d5 |
feat(amneziawg): add AmneziaWG as an outbound protocol (#6320)
* feat(amneziawg): add AmneziaWG as an outbound protocol - AmneziaWG outbound protocol end-to-end: config schema, socks bridge, netstack, panel UI - Route amneziawg outbounds to HTTP probe in TCP mode (backend + frontend classifiers) with pinning test - Add 2-minute idle read deadline to pumpUDPEgress to reap idle egress sessions - Require SOCKS5 username/password auth on the egress server (reject NO-AUTH with 0xFF) with test - Bound the egress TCP tunnel dial with portForwardDialTimeout (10s), matching portfwd.go - Resolve UDP domain targets off the association's reader loop via deliverUDPDatagram; race-safe getOrDial starts the reply pump at session creation; client passed by value into resolver goroutines (pinned by TestEgressUDPDatagramDomainInterleavedClients) - Reconcile early-returns on an empty desired set and closes the egress listener; EgressBasePort (64900) is reserved against local inbound port conflicts like the internal API port, with pinning tests for both the port reservation (TestCheckPortConflict_EgressPortBlockedLocal) and the Reconcile empty-desired Close/Listen lifecycle (TestOutboundManagerReconcileEmptyDesiredClosesEgress) - Eliminate acceptLoop shutdown race by validating listener != nil and registering to tracked under s.mu before wg.Add; bound pre-auth handshake with deadline (pinned by TestEgressServerCloseDuringConcurrentAccepts) - Support AAAA and dual-stack domain resolution in tunnel DNS resolver with v6 default fallback (DefaultTunnelDNSServerV6); add DNS field to frontend protocol form; avoid unneeded cache flushes on unchanged SetStack ticks * fix(amneziawg): resolve IPv6-only DNS default fallback and validate required keys - Default to IPv6 tunnel DNS on IPv6-only outbounds with blank dns - Require non-empty secretKey and peer publicKey in ValidateAmneziaWGOutbound - Add end-to-end IPv6 tunnel domain resolution test and test empty key rejection - Trim comment blocks exceeding 2 lines across modified files - Fix Storybook test execution on environments with POSIX locale Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
9f76a66dcf |
feat(sub): add dummy info node and status configs for subscriptions (#6412)
* feat(settings): add subInfoNodeEnable and status template settings * feat(sub): add dummy info node and status configs for raw links * feat(sub): support dummy info node in clash and json subscriptions * feat(ui): add subscription info node switch and status templates to settings * style: apply gofumpt formatting * fix(sub): address review feedback on subscription info node - Restore GetSubs contract to avoid unintended remark expansions on non-subscription-body calls. - Exclude dummy info node from Clash PROXY select group when active server nodes exist. - Track hasEnabledClient and set traffic.Enable in JSON and Clash paths so status tokens evaluate correctly. - Deterministically sort client emails across subscriptions before selecting primaryEmail. - Consolidate duplicated info-node evaluation logic into resolveInfoNodeRemark helper. - Remove redundant pure-getter test from setting_sub_info_node_test.go. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> |
||
|
|
540caa4e93 |
fix(hysteria): standard geco share links and persistent uTLS None (#6325)
- Export standard gecko obfs query params in hysteria2 share links - Enforce packet size bounds across Go and TypeScript link handlers - Persist uTLS None explicitly and initialize new TLS inbounds to chrome - Tear down stackTun safely without closeMu deadlock against WriteNotify Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> |
||
|
|
5d6d98d1f9 |
fix(warp): preserve WARP Plus license key when changing IP (#6218)
ChangeWarpIP rotated the WireGuard keypair by registering a brand-new Cloudflare device via RegWarp, which overwrites the stored warp data with the fresh registration's empty license_key. The old key was then re-applied only best-effort: any SetWarpLicense failure was swallowed with a warning log, permanently deleting the saved WARP Plus key, and even on success the response returned to the UI carried the pre-reapply snapshot (empty key). Fix: write the old license key back into the stored warp data immediately after RegWarp (before the remote upgrade attempt), so storage never loses it; keep the remote re-apply as best-effort but surface its failure as a warning field in the response; and return the final stored data so the modal shows the preserved key. The auto-update IP job shares this path and is fixed too. warpAPIBase is now a var so integration tests can point at a mock Cloudflare API. Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> |
||
|
|
694ad6deae |
feat(sub): add per-client subscription HWID limits (#5802)
* feat(sub): add per-client subscription HWID limits * fix(sub): address HWID review on shared subId and bulk create * fix(sub): store HWID devices by sub_id and drop anchor client workaround * fix(sub): restore UA auto-detect and HTML page routing in subs() The cherry-pick of the HWID gate onto main's refactored SUBController had dropped main's UA-based format auto-detection and sub-page handling from subs(). Restore those branches, slotting enforceHwid after the HTML page and before format detection so the gate only applies to machine-readable subscription bodies. Also adapt tests to main's options-struct constructor and to the ClientService.Update signature extended with limitHwid. * fix(frontend): drop axios from HttpUtil.delete The bulk-delete rework's committed version still referenced axios, which this file no longer imports, breaking typecheck in CI. Use the httpRequest wrapper like the other verbs. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
ea24ef0a69 |
feat(xray): default outbound in basic routing (#5815)
* feat(xray): default outbound picker in basic routing Let panel users choose which outbound handles unmatched traffic by moving it to the first position in the template outbounds list. * fix(xray): keep direct/blocked outbounds when changing default * style(routing): revert incidental whitespace churn Drop double blank lines and the reformatted function signature so the default-outbound diff stays focused on behavior. |
||
|
|
b8a654967f | Add encrypted DNS presets (#5837) | ||
|
|
15faec6258 |
fix(logs): limit Xray log growth (#5840)
* Limit Xray log growth * Apply suggestion from @github-actions[bot] Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply suggestion from @github-actions[bot] Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: Mahyar Dana <dana.mahyar76@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
82600936d6 |
fix(flow): restore XTLS Vision when an inbound becomes flow-eligible (#5520)
* fix(flow): restore XTLS Vision when an inbound becomes flow-eligible clientWithInboundFlow strips Vision from a VLESS client whenever the target inbound is not flow-eligible at client-write time — e.g. an XHTTP inbound before its vlessenc (ML-KEM) encryption is set, or a client attached to such an inbound. Nothing restored the flow once the inbound later became eligible: an inbound edit stores its settings verbatim and never re-gates the clients. So enabling encryption on an existing XHTTP inbound left every client without flow, and the generated configs, share links and subscriptions silently dropped flow=xtls-rprx-vision — most visibly on node inbounds and on any inbound where encryption was turned on after the clients existed. Restore the flow at the two points where an inbound can become eligible: - UpdateInbound: after the new stream/settings are final, re-add Vision to clients that currently carry no flow but whose intended flow (their flow_override on a sibling inbound, via EffectiveFlowByEmail) is Vision — only when the inbound is now flow-eligible. - MigrationRestoreVisionFlow: a one-time, idempotent boot migration that applies the same repair to existing installs and refreshes flow_override via SyncInbound. The repair is conservative: it never invents a flow for a client that has none anywhere, never overwrites an explicit flow, and is a no-op on healthy installs. Adds EffectiveFlowByEmail and a unit test covering keep/skip/no-op cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(flow): serialize restored settings with MarshalIndent Match the indented JSON used by the adjacent timestamp block in UpdateInbound and the externalProxy migration, so a restored inbound's settings column keeps the same multi-line format as everything else (review nit on #5520). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(flow): batch the intended-flow lookup and run it on the active tx restoreVisionFlowForEligibleInbound resolved each empty-flow client's intended flow with EffectiveFlowByEmail, which issued two queries per client (GetRecordByEmail + EffectiveFlow). A client that genuinely uses no Vision keeps an empty flow forever, so it was re-queried on every UpdateInbound and every boot — O(clients) queries per save on a Reality/TCP or XHTTP+vlessenc inbound carrying many non-Vision clients, executed inside the serialized writer transaction. Replace it with EffectiveFlowsByEmails: collect every empty-flow email first and resolve them in a single batched join over client_inbounds + clients (lowest inbound_id wins, same rule as before), chunked for the SQLite bind-var limit. Also thread the active tx through restoreVisionFlowForEligibleInbound so the read runs on the writer's own connection while it holds the lock instead of a separate pooled connection (UpdateInbound passes its tx; the boot migration passes nil → GetDB() as before). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
14de0557f9 |
feat(clients): bulk-set XTLS flow from the Adjust dialog (#5524)
* feat(clients): bulk-set XTLS flow from the Adjust dialog Add a "Set flow" dropdown to the bulk Adjust dialog so an admin can set or clear the XTLS flow on all selected clients at once, alongside the existing days/traffic bumps. Empty by default (no effect on save); "Disable" clears flow, and the two vision values mirror the per-client credential tab. Flow rides the existing inbound-JSON -> SyncInbound path (ClientRecord.Flow + client_inbounds.flow_override), so no new endpoint, DB column, or migration. Setting a vision flow is gated by inboundCanEnableTlsFlow: ineligible inbounds are left untouched and reported as skipped; clearing is always allowed. A real flow change requests an xray restart (local) or a node reconcile (remote). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(clients): keep days/traffic write when bulk flow is ineligible Address review on the bulk-flow-adjust PR: - Blocking: a client adjusted with both a days/traffic delta and a flow directive on a flow-ineligible inbound had the flow-ineligibility recorded into the same skip set that gates the ClientTraffic write, so the inbound JSON / ClientRecord advanced but ClientTraffic did not — divergent stores, and the client misreported as skipped. Track flow ineligibility in its own map (bulkInboundAdjustResult.flowIneligible) so it only feeds the final Skipped report and never suppresses the expiry/total persistence. - Drop the broad delete(skippedReasons, email): flow reasons no longer enter skippedReasons, so honoring a flow can no longer erase an unrelated skip reason (unlimited expiry, a real persistence error on another inbound). - Drop the inline comment block from ClientBulkAdjustModal.tsx (file had none); move the whitelist-sync note next to bulkFlowAllowed, the source of truth. - Document the optional flow field in the bulkAdjust API-docs example (endpoints.ts) and regenerate openapi.json. - Add a regression test covering days+flow on an ineligible inbound. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c93beef267 |
fix(inbounds): accept null rewritePort in tunnel settings (#5516) (#5525)
Clearing the Rewrite port field makes AntD InputNumber write null into the form store. The tunnel schema declared rewritePort as PortSchema.optional(), which accepts undefined but not null, so saving (or the JSON tab reflecting null) failed validation with "settings.rewritePort — Invalid input". Accept null and collapse it to undefined so the field is simply omitted from the serialized payload, matching the behavior of deleting the key by hand. The trailing .optional() keeps the key optional in the inferred type. Closes #5516 |
||
|
|
fea3c94b11 |
feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) (#5506)
* feat(xhttp): support sessionID* rename + sessionIDTable/Length (xray v26.6.22) xray-core v26.6.22 (PR #6258) renamed the XHTTP session config keys sessionPlacement/sessionKey to sessionIDPlacement/sessionIDKey (no fallback kept in core) and added sessionIDTable (predefined charset name or literal ASCII) and sessionIDLength (range, e.g. 16-32, lower bound > 0). Panel changes: - Schema (xhttp.ts): rename the two keys, add sessionIDTable/sessionIDLength, and a z.preprocess that lifts legacy keys off stored configs so an upgraded panel never silently drops a saved session setting. - Wire normalize + share-link build/parse: rename keys, emit the two new fields, and accept legacy sessionPlacement/sessionKey from old share links. - Inbound + outbound XHTTP forms: rename field paths, add a sessionIDTable autocomplete (9 predefined tables + free ASCII) and a sessionIDLength range input shown only when a table is set, with light client validation (ASCII table, length min > 0; xray enforces the room-size minimum server-side). - Subscription (service.go) and Clash (clash_service.go) builders: emit the renamed + new keys, with a legacy fallback for not-yet-resaved inbounds. - Locales: add sessionIDTable/sessionIDLength labels + hints in all 13 files. Two sibling v26.6.22 XHTTP commits need no panel change and are covered by the core bump alone: #6332 (XHTTP/3 closes QUIC/UDP) and #6320 (udpHop honors the existing dialerProxy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(xhttp): add Session ID Table to inbound form-blocks snapshot The new sessionIDTable input renders by default in the inbound XHTTP form, so its label joins the field-structure snapshot. sessionIDLength stays conditional (only shown when a table is set), so it does not appear here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(xhttp): migrate legacy session keys in the running xray config The Zod preprocess plus the subscription/Clash fallbacks only covered the panel UI and share-link output. The config handed to the running xray-core process is built from the raw stored streamSettings in GetXrayConfig, which did not rewrite the renamed XHTTP session keys — so a pre-upgrade inbound (or template outbound) stored with a non-default sessionPlacement was emitted unchanged and dropped by xray-core v26.6.22, until the admin re-saved it. Lift sessionPlacement/sessionKey onto sessionIDPlacement/sessionIDKey at config-generation time, in the existing inbound stream-rewrite block (next to the tls/reality/externalProxy handling) and across template outbounds. The lift is idempotent and leaves unchanged configs byte-identical so the hot-reload diff never sees a spurious change. Also tighten validateSessionIDLength to reject an inverted range (e.g. 32-16) in addition to the existing lower-bound > 0 check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(xray): avoid summed-capacity allocation in mergeSubscriptionOutbounds CodeQL go/allocation-size-overflow flagged the pre-sized make() whose capacity was a sum of three slice lengths. Grow the slice via append on a nil slice instead; same result, no overflow-prone capacity expression. |
||
|
|
b07fad0e69 |
refactor(wireguard): drop removed workers field (xray v26.6.22) (#5509)
* v3.4.0 * refactor(wireguard): drop removed `workers` field (xray v26.6.22) xray-core v26.6.22 (PR #6287) removed the WireGuard `workers` (num_workers) config field; the engine now relies on wireguard-go's internal worker fallback and no longer reads it. Remove it from the panel so it stops emitting a key xray ignores. Removed from the inbound/outbound/outbound-form WireGuard schemas, both WireGuard forms, the outbound form adapter (both directions) and defaults, the two affected tests, and the `workers` label in all 13 locales. Existing configs that still carry workers are simply dropped on parse — no migration needed since the field had no runtime effect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update version --------- Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a0f4c13dc5 |
fix(sockopt): honor trustedXForwardedFor on gRPC inbounds (xray v26.6.22) (#5503)
* fix(sockopt): honor trustedXForwardedFor on gRPC inbounds xray-core v26.6.22 (commit 711aea4) switched the gRPC server from reading the x-real-ip gRPC metadata to resolving the client IP from X-Forwarded-For via sockopt.trustedXForwardedFor, matching ws/httpupgrade/xhttp. The panel already exposed the trustedXForwardedFor field and wire output, but the per-transport gate (TRUSTED_HEADER_NETWORKS) still omitted grpc. On a gRPC inbound this raised a false "transport does not honor this header" warning and mis-flagged the Cloudflare real-client-IP preset. Add grpc to the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(i18n): note gRPC in trustedXForwardedFor hint (all locales) Follow-up to the gRPC gate fix: the trustedXForwardedForHint tooltip across all 13 locales said the header is honored "only on WebSocket, HTTPUpgrade and XHTTP". xray-core v26.6.22 added gRPC, so list it too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
628406117e |
fix(nodes): sync "start after first connect" expiry so un-activated nodes do not reset it (#5319)
* fix(nodes): stop un-activated nodes from resetting "start after first connect" expiry In a multi-node setup a client is attached to inbounds on several nodes, but its `client_traffics` row is shared per-email (the column is `gorm:"unique"`). With "start expiry after first connect", the expiry is stored as a negative duration and each node converts it to an absolute deadline (now+duration) the first time the client connects *there*. The master's per-node traffic merge wrote `expiry_time = ?` unconditionally for every node sync. So a node where the client never connected keeps reporting the un-activated negative duration and clobbers the absolute deadline that the node where the client *did* connect had already activated — last writer wins. The shared row flip-flops and usually lands back on the negative value, so the main panel shows the timer "not started" while the active node counts down, and the subscription (which reads this row and recomputes negative as now+duration on every fetch) reports a perpetually-resetting, wrong expiry and usage. Guard the merge so an un-activated (<= 0) value reported by a node can never reset an already-activated absolute deadline. A positive node value is still adopted, so a node that legitimately moves the deadline forward (traffic reset / auto-renew) still propagates. The rule lives in both the SQL CASE used by the merge and a small `mergeActivationExpiry` helper (kept in lockstep) that the structural-change check reuses so the guard does not trigger spurious config re-pushes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(nodes): cast expiry merge params to BIGINT for Postgres The "start after first connect" merge guard introduced the comparison `? <= 0` in the client_traffics expiry_time CASE. There Postgres infers the parameter type as int4 from the literal 0, so binding a real expiry value — a negative start-after-connect duration or a positive absolute deadline (~1.7e12 ms) — overflows int4 and the whole setRemoteTrafficLocked transaction fails, breaking node traffic and expiry sync on Postgres. SQLite (dynamic typing) was unaffected. Wrap both params in CAST(? AS BIGINT) (portable across SQLite and Postgres) so the parameter is typed bigint, matching the explicit casts the sibling GreatestExpr/ClientTrafficEnableMergeExpr helpers already use. Verified against Postgres 16: TestNodeFirstConnectExpiry_NotClobbered failed before this change and passes after; SQLite suite unchanged. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
dab0add191 |
feat(finalmask): support Salamander packetSize (Gecko) and Realm tlsConfig for Hysteria2 (#5278)
* feat(finalmask): support salamander packetSize (Gecko) and realm tlsConfig Hysteria v2.9.1/v2.9.2 added two finalmask features that the pinned Xray-core (26.6.1, 94ffd50) already supports but the panel UI did not expose: Salamander's packetSize range (Gecko, XTLS/Xray-core#6198) and the Realm UDP hole-punching mask's optional tlsConfig (XTLS/Xray-core#6137). Add typed schemas and form fields for both, keeping UdpMaskSchema.settings permissive per the existing finalmask design note. packetSize reuses the existing dash-range preprocess (like udpHop.ports) so it round-trips under the fm= share-link param with no new URI key; realm tlsConfig emits xray's flat TLSConfig shape (serverName/alpn/fingerprint/allowInsecure). Verified against the bundled Xray 26.6.1: configs with packetSize and realm tlsConfig validate (Configuration OK.), plain salamander stays backward-compatible, and a malformed packetSize is correctly rejected by the salamander mask builder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(finalmask): add snapshots for salamander-gecko and realm-tls fixtures vitest run does not auto-create missing snapshots in CI mode, so the two new fixtures need committed snapshot entries. Verified under node:22 that finalmask.test.ts passes (6/6) with these snapshots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(finalmask): polished Gecko UX with core-grounded validation Fold PR #5281's Gecko work into the Realm tlsConfig base: - Replace the plain packetSize input with a Salamander/Gecko mode selector and validated Min/Max number inputs. - parseGeckoPacketSize enforces xray-core's real bound (1 <= min <= max <= 2048, the gecko buffer size) so the panel rejects configs core would reject at runtime. - Accurate Gecko description; add parser unit tests. - Drop the unused Salamander/Realm settings schemas; settings stay permissive and are validated at the form level. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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
|