mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 06:06:08 +00:00
33aada0c7c66bb95495eb269a5993b9f44f040d1
74 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
33aada0c7c |
feat(xhttp): default xmux maxConnections to 6
xray-core v26.6.27 changed the XHTTP client xmux default to maxConnections=6 (anti-RKN). The panel previously sent maxConnections=0, which overrode that default; default XHttpXmuxSchema to 6 so new outbounds adopt it and the wire-exclusivity rule drops maxConcurrency accordingly. |
||
|
|
797b08cd07 |
fix(balancers): create burst observer for random/roundRobin with fallbackTag
xray-core's Random/RoundRobinStrategy calls RequireFeatures(Observatory) whenever a fallbackTag is set, so a balancer that declares a fallback but has no observatory aborts startup with 'core: not all dependencies are resolved'. syncObservatories never created an observer for these strategies, crashing the core on any load balancer that used a fallback (the default 'random' strategy with a fallbackTag, exactly issue #5605). Treat random/roundRobin balancers that set a fallbackTag as requiring the burst observer. Also make the burst observer strictly requirement-driven (mirroring the leastPing/observatory path) so clearing the last fallbackTag drops it again instead of leaving a dead observer that forces needless restarts and probing. Closes #5605 |
||
|
|
7a2179535a |
fix(settings): normalize API token timestamps (#5599)
* fix(settings): normalize API token timestamps * refactor(api-token): share timestamp threshold --------- Co-authored-by: Tomilla <5007859+Tomilla@users.noreply.github.com> |
||
|
|
6964d84742 |
feat(reality): add live REALITY target scanner with IP/CIDR discovery
Replace the static reality-targets list with a server-side TLS 1.3 probe that checks TLS 1.3 + HTTP/2 + X25519 + a trusted certificate. - Single-domain validate auto-fills target and serverNames from the cert SAN - Discovery scans an IP/CIDR without SNI to find new targets from their certificates, deduped and ranked by feasibility then latency, private-IP guarded via netsafe - New endpoints scanRealityTarget and scanRealityTargets with RealityScanResult, plus openapigen and api-docs entries - Add scanner strings to all 13 locales - Replace deprecated AntD Alert message prop with title across the panel |
||
|
|
e4b881e58a |
feat(panel): surface dev-build version in UI, bot, and CLI
A dev build now shows its `dev+<commit>` identity instead of a misleading stable-looking version in the sidebar badge, dashboard card, update modal, Telegram status report, startup log, and `x-ui -v`. Adds a shared formatPanelVersion helper (single v prefix; dev labels shown verbatim) and fixes the mobile-tag double-v. Renames the version getters for clarity: config.GetVersion to GetBaseVersion (raw embedded version), config.GetReportedVersion to GetPanelVersion (advertised/displayed), and the xray process GetVersion to GetXrayVersion. |
||
|
|
bd60e770f4 |
fix(outbound): preserve custom headers for HTTP outbounds (#5519)
The Outbounds form routed HTTP through the SOCKS-shared simpleAuth adapter, which only knew address/port/user/pass, so xray's top-level settings.headers was dropped on both load and save. Opening and re-saving an HTTP outbound destroyed its headers. Add headers to the HTTP wire/form schemas, round-trip it via dedicated httpFromWire/httpToWire helpers, and expose a HeaderMapEditor in the form. Only settings-level headers round-trip; xray-core ignores per-server headers. |
||
|
|
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> |
||
|
|
852b53db79 |
feat(xray): add loopback sniffing and per-segment fragment masks
- Loopback outbound: add sniffing support (xray-core #6320) - FinalMask fragment: support per-segment lengths/delays arrays with legacy length/delay migration (xray-core #6334) - Consolidate sniffing into a shared SniffingFields component and the canonical SniffingSchema across inbound, VLESS reverse, and loopback |
||
|
|
679d2e1cca |
fix: resolve a batch of open bug-tagged issues (traffic accounting, share strategy, sub address, CPU) (#5477)
* fix(node): never re-add a node's full counter on reset/restart (#5456, #5476, #5390) When a node's per-client counter dips below the master's stored baseline (node reboot, xray restart, or a reset propagated to the node), the delta accounting clamped delta to the node's whole current counter and re-added it to the master total — double-counting a client's lifetime usage in a single sync and often pushing them over quota. Treat a backward-moving counter as a reset: add 0 and rebaseline to the reported value, so only genuine post-reset usage accrues. Resets also now clear the per-node NodeClientTraffic baseline (ResetClient TrafficByEmail, resetClientTrafficLocked, BulkResetTraffic, resetAllClient TrafficsLocked), mirroring the delete paths. Without this the node's pre-reset cumulative — including traffic it had counted but not yet synced — leaks back onto the master after a reset, which is the 'reset reverts after a while' report. The next sync then takes the clean delta=0 + rebaseline path regardless of node state. Updates TestNodeCounterReset (was _Clamped, now _NoReAdd) to assert rebaseline instead of re-add, and adds TestCentralResetClearsNodeBaseline_NoLeak. * fix(inbound): keep persisted node share strategy on edit (#5375) Opening the edit modal silently reverted shareAddrStrategy from 'node' to 'listen'. The downgrade effect fires before the form settles: availableNodes is an empty placeholder until /nodes/list resolves, and Form.useWatch('protocol') is briefly empty on the first edit render — both transiently make the node option look unavailable, so the effect clobbered the saved value. Gate the downgrade on availableNodesFetched (threaded from useNodesQuery through InboundsPage) and on the protocol watch being settled, so a persisted strategy is only downgraded when the node option is genuinely unavailable. Adds a rerender-based regression test covering the nodes-loading race. * <3 * perf(traffic): skip cross-panel quota subquery when no globals exist (#5392, #5389) disableInvalidClients ran a correlated EXISTS against client_global_traffics on the full client_traffics table every 5s. On a panel no master pushes to, that table is empty so the subquery can never match — yet it forced a full scan that pegged Postgres at 100% CPU on large client counts. Probe the table first and drop the EXISTS branch when it's empty (the common case), and add an idx_client_global_email index so the subquery is an index lookup when globals are present. Cross-panel enforcement is unchanged (TestGlobalUsage_DisablesClient). This also relieves #5389 ('traffic writer queue full' / panel freeze): the heavy query runs inside the serialized traffic write, so a slow DB backs the shared writer queue up until request handlers block. * fix(sub): don't advertise a leaked client IP for local wildcard inbounds (#5425) For a local inbound with no node, no custom share address, and a wildcard/blank listen, resolveInboundAddress fell straight through to the subscriber's request host. Behind NAT/proxy/CDN that Host can be the requesting client's own IP, so the subscription wrote the client's address into the inbound instead of the server's — while the panel's own share link (which doesn't use the request host) stayed correct. Prefer the admin's configured public host (Sub/Web domain) over the raw request host for this last-resort fallback. With no configured host the request host still stands, so existing single-domain setups are unaffected. |
||
|
|
39774a6a38 |
fix(tls): default OCSP stapling to off for new inbound certs
Certs without an OCSP responder URL (e.g. Let's Encrypt, which dropped OCSP in 2025) made xray log 'ignoring invalid OCSP: no OCSP server specified in cert' on every refresh. Default the per-cert ocspStapling interval to 0 (disabled) so new inbounds stay quiet; the field is kept for certs that do support stapling. |
||
|
|
7c8889466b |
feat(tls,reality): port xray TLS/REALITY fields, cert-hash helpers, fallback UX
TLS: add verifyPeerCertByName (vcn) to inbound settings + emit in both share-link generators (frontend + Go sub) and outbound parser; the allowInsecure replacement xray removed after 2026-06-01. Add server-side curvePreferences, masterKeyLog, echSockopt (passthrough + form) at tlsSettings top-level so they survive the panel-only settings strip. REALITY: add limitFallbackUpload/Download (afterBytes/bytesPerSec/burstBytesPerSec) with per-field tooltips, plus masterKeyLog. Verified field names/semantics against pinned xray v1.260327.1 (bytesPerSec=0 disables). Hosts: fix verify_peer_cert_by_name column bool->string (xray expects comma-separated names) with an idempotent, history-gate-free migration (SQLite typeof blank; Postgres ALTER once); emit vcn for hosts/external proxies. Server: add getCertHash (local cert DER SHA-256) and getRemoteCertHash (xray tls ping) endpoints + api-docs; wire pinned-cert field buttons. Drop the meaningless random-hash button. Xray UI: metrics endpoint (listen/tag) config in Basics; import/export for routing rules and outbounds. Fallbacks card: compact empty state, header-aligned actions, responsive labeled grid rows. i18n: add all new keys to every locale; drop unused generateRandomPin. |
||
|
|
a5bc71a6f1 |
fix(sub): SS2022 share links must not base64-encode userinfo (#5432)
Per SIP022, ss:// links for 2022-blake3-* methods must NOT base64-encode the userinfo; method and password are percent-encoded instead. Clients like Hiddify reject the base64 form. Fix both the server-side subscription path and the client-side panel link, plus the matching parsers for round-trip import. |
||
|
|
d01d9867e4 |
fix(sub): preserve non-default scMinPostsIntervalMs and use per-inbound xmux in JSON subscriptions (#5393)
* fix(sub): preserve non-default scMinPostsIntervalMs in inbound wire payload
The frontend wire normalizer unconditionally deleted scMinPostsIntervalMs
from inbound configs before persisting to the database, so JSON
subscriptions could never include it — even when the admin set a
non-default value like "50-150".
Only strip the xray-core default ("30") or empty values. The literal
"30" is a known DPI fingerprint (#5141) and must still be removed, but
custom tuning knobs must survive the round-trip so that buildXhttpExtra
and the JSON subscription generator can propagate them to clients.
Add tests for non-default preservation and empty-value stripping.
* fix(sub): use per-inbound xmux instead of global subJsonMux in JSON subscriptions
The JSON subscription generator always used the global subJsonMux panel
setting for outbound.Mux, even when the inbound carried per-inbound xmux
inside xhttpSettings. This meant XHTTP outbounds that configured their own
multiplexing via xmux still got the legacy mux.cool block injected — and
the inbound's own xmux was silently ignored.
Now getConfig() checks whether xmux is present in the inbound's
xhttpSettings. When it is, the per-inbound xmux handles multiplexing
and the legacy outbound.Mux is suppressed. When xmux is absent, the
global subJsonMux is used as before.
The mux selection is threaded through genVless, genVnext, genServer,
and genHy as an explicit parameter so each protocol handler can decide
independently.
Add tests:
- xmux present → outbound.Mux suppressed, xmux survives streamData()
- no xmux → global subJsonMux used as outbound.Mux
* feat(ui): add scMinPostsIntervalMs to inbound XHTTP form
The inbound XHTTP form was missing scMinPostsIntervalMs, making it impossible
for admins to configure this client-only tuning knob through the panel. The
field already existed in the Zod schema and outbound form, and the wire
normalizer (PR #5393) now preserves non-default values for subscription
propagation.
Add Form.Item for scMinPostsIntervalMs in the packet-up section of the
inbound XHTTP form, after scMaxEachPostBytes. Use the existing translation
key and a placeholder that shows the range format without endorsing the
DPI-fingerprinted default (30).
Update the Zod schema comment to clarify that scMinPostsIntervalMs is now
preserved on inbound for subscriptions, while uplinkChunkSize and
noGRPCHeader remain outbound-only.
Add two integration tests:
- Non-default value (50-150) preserved through formValuesToWirePayload
- Default value (30) stripped through the full pipeline
* fix(ui): show packet-up fields for auto mode in inbound XHTTP form
When mode is 'auto', the server accepts all three XHTTP modes including
packet-up. The packet-up-specific fields (scMaxBufferedPosts,
scMaxEachPostBytes, scMinPostsIntervalMs) are therefore relevant and
should be configurable.
Change the conditional from 'packet-up' only to
'packet-up || auto' so admins using the default 'auto' mode can
configure these fields.
* fix(outbound): show scMinPostsIntervalMs for auto mode, update placeholder
- Show scMinPostsIntervalMs field when mode is 'auto' in addition
to 'packet-up', since auto+TLS resolves to packet-up client-side
- Change placeholder from '30' (DPI fingerprint) to 'e.g. 50-150'
for consistency with inbound form
* fix(inbound): show scMaxEachPostBytes for all modes, gate scMaxBufferedPosts behind packet-up/auto
scMaxEachPostBytes is used by xray-core in every mode (both handlePacketUp
and handleStreamUp validate it) and must be visible regardless of mode.
scMaxBufferedPosts is only used by handlePacketUp, so it remains gated
behind the packet-up/auto conditional.
Also show scMinPostsIntervalMs for auto mode in outbound form and change
placeholder from '30' (DPI fingerprint) to 'e.g. 50-150'.
Update snapshot to reflect the new field order.
* fix(inbound): correct XHTTP field visibility per xray-core source verification
- scMaxEachPostBytes: move behind packet-up/auto gate (server only checks
it in handlePacketUp, not handleStreamUp)
- scMaxBufferedPosts: show for packet-up, stream-up, and auto (server
uses uploadQueue in both handlePacketUp and handleStreamUp)
- scStreamUpServerSecs: already correct (stream-up only)
Verified against xray-core hub.go and dialer.go source code.
---------
Co-authored-by: w3struk <w3struk@gmail.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
|
||
|
|
709b332d17 |
feat(hosts): managed Hosts for per-host subscription link overrides (#5409)
* test(sub): characterize current link output (externalProxy + single-link baselines)
Phase 0 of the Hosts feature. Locks current subscription-link output for the
externalProxy paths (vless/vmess/trojan/ss exact, reality/hysteria by Contains)
so the upcoming ShareEndpoint refactor can be proven behavior-preserving. These
must stay green and unedited through every later phase.
* refactor(sub): unify external-proxy link building behind ShareEndpoint (TDD, snapshot-locked)
Phase 1 of the Hosts feature. Collapse the duplicated externalProxy link
builders (param-form for vless/trojan/ss, object-form for vmess) onto a single
ShareEndpoint abstraction so Phase 4 can add Host-driven links with ~zero new
branching.
Design: an externalProxy-derived endpoint carries the original entry map and
applies it through the UNCHANGED applyExternalProxyTLS{Params,Obj} helpers, so
output is provably byte-identical. buildExternalProxyURLLinks /
buildVmessExternalProxyLinks become thin adapters; the genVless/Trojan/SS/Vmess
call sites are untouched. genHysteriaLink is deliberately left on its own path
(hex pinSHA256, not pcs). The no-externalProxy default tails are unchanged.
TDD: N1-N4 (externalProxyToEndpoint, inboundDefaultEndpoint, buildEndpointLinks,
buildEndpointVmessLinks) written failing-first against stubs, then implemented.
Mutation sanity (performed + reverted): dropping the ep-carry in
externalProxyToEndpoint makes the Phase-0 C1/C2 characterization snapshots go
red (TLS overrides vanish), proving the snapshots guard the emitted output.
Gate: go test ./internal/sub/... and go test ./... green with ZERO edits to the
Phase-0 snapshots; go build ./... green on linux and windows; go vet clean.
* feat(model): Host entity + automigrate + openapi codegen (TDD)
Phase 2 of the Hosts feature. Adds the Host GORM model: an override endpoint
attached to an inbound (address/port + TLS/transport/clash overrides + sub
scoping), superseding the legacy externalProxy array functionally while leaving
it intact.
- model.Host with snake_case column tags, json serializer for slices, text for
free-JSON (mux/sockopt/xhttp), validate tags (remark 1-40, port 0-65535,
security + mihomoIpVersion enums); TableName "hosts". NodeGuids column is added
now but unused (host->node scoping deferred to v2).
- Registered in BOTH initModels() (db.go) and migrationModels() (migrate_data.go);
the latter is required for cross-DB migration and is easy to miss. PG sequence
resync iterates the initModels slice, so it is covered automatically.
- pruneOrphanedHosts() deletes hosts whose inbound_id has no inbound, called
alongside pruneOrphanedClientInbounds().
- openapigen manifest: Host added to StructAllow with MuxParams/SockoptParams/
XhttpExtraParams -> KindAny; regenerated frontend/src/generated/* + openapi.json.
TDD: TestHostTableName, TestHostValidation, TestHostAutoMigrateCreatesColumns
(+ _Postgres), TestPruneOrphanedHosts written failing-first against a wrong-name,
untagged, unregistered stub, then implemented.
Gate: go test ./... green on SQLite AND a real Postgres DSN (local container);
go build/vet/gofmt clean; npm run gen succeeds with the new Host type/schema/
example/zod; npm run typecheck + npm run test (542) green.
* feat(api): Host CRUD service + controller + routes (TDD)
Phase 3 of the Hosts feature.
- service/host.go (HostService, empty struct + database.GetDB() like
ClientService): GetHosts, GetHostsByInbound, GetHost, AddHost (verifies the
inbound exists — no hard FK), UpdateHost (inbound + sort order immutable here),
DeleteHost, SetHostEnable, SetHostsEnable, DeleteHosts, ReorderHosts (single
driver-safe transaction), GetAllTags.
- controller/host.go mirrors NodeController: routes under /panel/api/hosts
(list/get/byInbound/tags + add/update/del/setEnable/reorder + bulk/setEnable,
bulk/del), binds via middleware.BindAndValidate so the model validate tags are
enforced, {success,msg,obj} envelopes.
- Wired the hosts group into api.go after nodes (inherits checkAPIAuth + CSRF).
- DelInbound now cascades: deleting an inbound deletes its hosts.
- Documented all 11 routes in api-docs endpoints.ts (referencing the generated
Host schema) and regenerated openapi.json; extended TestAPIRoutesDocumented's
controller->basePath switch for host.go. Backend en toast keys added.
TDD: service tests (Add/GetByInbound, RejectsUnknownInbound, Reorder, Set/Bulk
enable, DeleteHosts, DeleteInboundCascadesHosts, GetAllTags) written failing-
first against a nil-returning stub; controller test (AddListGetDelete envelope
round-trip + AuthInherited 401) added.
Gate: go test ./internal/web/... + go test ./... green; npm run gen + typecheck
+ lint + test (542) + build green.
* feat(sub): render subscription links from hosts; legacy fallback when none (TDD, mutation-checked)
Phase 4 of the Hosts feature. Inserts host resolution between inbound and link
across all three subscription formats.
Mechanism: hostEndpoints(inbound, format) loads the inbound's enabled hosts
(filtered by ExcludeFromSubTypes, ordered by sort_order then id) and projects
each onto the externalProxy entry shape the raw/json/clash renderers already
consume. So a host fans out one link/proxy reusing the exact existing rendering
(address/port/security/sni/fp/alpn/pins/ech) with zero new TLS code. Host header
and path overrides are applied additively in the raw builders (no-op for legacy
externalProxy, which never carries those keys — characterization snapshots stay
green). Clash ip-version (MihomoIpVersion) is set last on the proxy.
Integration points:
- getSubs (raw): per inbound, hostEndpoints AFTER projectThroughFallbackMaster;
len>0 -> linkFromHosts (renders only the hosts), else legacy GetLink.
- GetJson/GetClash: inject the host endpoints into the inbound's externalProxy
before the existing getConfig/getProxies loop.
- Precedence: hosts win over any legacy externalProxy (injection replaces it).
Backward compat: a zero-host inbound takes the legacy path -> byte-identical
output (all Phase-0 characterization snapshots unchanged).
TDD: 9 cycles (zero-hosts identical, N-links-ordered with host/path override,
disabled skipped, host-vs-externalProxy precedence, no-dedup, sort composes with
SubSortIndex, host-over-fallback, resolve-via-client-inbounds, ExcludeFromSubTypes
per format) written failing-first against unwired helpers, then wired green.
Mutation sanity (performed + reverted, documented here):
- zero-hosts fallback: flipping the len(hostEps)>0 guard to >=0 makes
TestSub_ZeroHosts_IdenticalOutput go red (host path yields "" for no hosts).
- no-dedup: adding a remark-dedup in hostEndpoints makes TestSub_NHosts_NoDedup
go red (two distinct hosts collapse to one link).
Gate: go test ./internal/sub/... + go test ./... green with ZERO edits to the
Phase-0 snapshots; go build green on linux and windows; go vet + gofmt clean.
* feat(migration): seed hosts from inbound externalProxy (TDD, idempotent, dual-driver)
Phase 5 of the Hosts feature. One-time migration so existing installs surface
their legacy externalProxy entries as first-class Host rows.
- seedHostsFromExternalProxy() is self-gated on a HistoryOfSeeders
"HostsFromExternalProxy" row (run-once) and wired into runSeeders. For each
inbound it parses StreamSettings, reads externalProxy[], and creates one Host
per entry: forceTls->Security (unknown->same), dest->Address, port->Port,
remark->Remark (generated when blank, capped at 40), sni/fingerprint/alpn/
pinnedPeerCertSha256/echConfigList copied; SortOrder=index; InboundId set.
- Additive: externalProxy is left intact in StreamSettings (rollback-safe; the
sub layer prefers hosts when present, §Phase 4).
- Postgres: GORM db.Create advances hosts_id_seq via the sequence, so no extra
resync is needed beyond the existing startup resync.
TDD: field-mapping, idempotency (second run no-op), no-externalProxy->no-hosts,
externalProxy-kept-intact written failing-first against a stub; plus a
Postgres counterpart that skips without XUI_DB_DSN.
Gate: go test ./internal/web/service/... ./internal/database/... green on SQLite;
the *_Postgres tests green against a real Postgres container; go build green on
linux and windows; go vet + gofmt clean. (Running the whole database package
under XUI_DB_TYPE=postgres is not supported — the SQLite-path tests share the one
DSN — so only the t.Skip-gated *_Postgres tests run with the env set.)
* feat(ui): Hosts page + schema + query hooks + link preview helper (TDD on schema/helpers)
Phase 6 of the Hosts feature — the admin UI.
- schemas/api/host.ts: HostFormSchema (validation: remark 1-40, tags ^[A-Z0-9_:]+$
≤10×≤36, port 0-65535, security/mihomoIpVersion enums, alpn/fingerprint reused
from the shared primitives) + a loose HostRecordSchema/HostListSchema for reads.
- lib/hosts/host-link.ts: hostToExternalProxyEntry — the frontend mirror of the
backend hostToExternalProxyMap (security->forceTls, sni override rules, port
inherit), for share-link previews.
- api/queries/useHostsQuery.ts + useHostMutations.ts (mirror the node hooks):
list/get + add/update/del/setEnable/reorder/bulk; queryKeys.hosts.* added;
mutations invalidate keys.hosts.root().
- pages/hosts/{HostsPage,HostList,HostFormModal}.tsx (+CSS) mirroring pages/nodes:
list with remark · address:port · inbound · security · tags · enable Switch ·
per-inbound move up/down (reorder) · bulk enable/disable/delete; form grouped
into Basic / Advanced / Clash / Subscription-scope sections.
- Route '/hosts' + sidebar item (Global icon); menu.hosts + pages.hosts.* added to
the en-US bundle (other locales fall back to English until translated).
TDD: HostFormSchema (10 cases) and hostToExternalProxyEntry (6 cases) written
failing-first, then implemented. UI verified by lint/typecheck/test/build.
Deferred (documented enhancement): the live in-form share-link preview (needs
inbound+client context) and a per-host host/path override in JSON/Clash output
(raw already overrides; JSON/Clash inherit the inbound's host/path).
Gate: cd frontend && npm run lint && npm run typecheck && npm run test (557) &&
npm run build all green; go build ./... + go test ./... still green.
* refactor(ui): remove the External Proxy form from the inbound stream settings
Hosts supersede the legacy externalProxy: the subscription renders from hosts
(hosts win when both exist) and the migration converts existing externalProxy
entries to hosts. externalProxy's only real consumers were the subscription
(now covered) and this form's preview — the backend per-client copy-link never
used it — so removing the editor has no functional regression.
- Drop ExternalProxyForm + toggleExternalProxy from InboundFormModal and delete
the orphaned form component + its export; remove its block test + snapshot.
- KEEP the externalProxy schema field and backend parsing/link-generation: an
existing inbound's externalProxy still round-trips through the form (not
silently destroyed on edit) and still renders if a host was removed.
Gate: cd frontend && npm run typecheck + lint + test (556) + build green.
* fix(ui): use Alert `title` instead of deprecated `message` (antd 6)
Ant Design 6 deprecated <Alert message=> in favor of <Alert title=>; the panel
was mid-migration (21 Alerts already on title). Renamed the 7 remaining stragglers
across 5 files (SubLinksModal, InboundFormModal, sockopt, EmailTab, TelegramTab),
silencing the runtime deprecation warning. description= is unchanged.
Pre-existing warning, surfaced while testing Hosts — not introduced by it.
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): align Hosts page with Clients/Inbounds cards + reorder columns
- page-shell.css never listed .hosts-page, so the Hosts page got no content
padding / transparent-layout / summary-card spacing. Add a .hosts-page shell
block (background, dark/ultra vars, content-area + summary-card padding). This
is the actual "card spacing" bug.
- HostList: match the Clients/Inbounds list card — hoverable + the toolbar moved
into the card title as a .card-toolbar (Add when nothing selected; selected
count + bulk enable/disable/delete on selection). Re-declare .card-toolbar in
HostList.css since the shared rule lives in a lazily-loaded page stylesheet.
- Reorder table columns as requested: Actions, Enable, then Remark, Endpoint,
Inbound, Security, Tags. Added scroll x for narrow screens.
- HostsPage: add a summary card (Total / Enabled / Disabled) like the other
pages. New i18n keys: pages.hosts.selectedCount + pages.hosts.summary.*.
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): use Tabs instead of Collapse in the Add/Edit Host form
The Basic / Advanced / Clash / Subscription-scope sections are now tabs. Each
pane sets forceRender so all fields stay mounted — required because the form
uses preserve=false, so an unmounted tab's values would otherwise be dropped on
submit (and a required field on a hidden tab still blocks submit).
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): split Host form into Security + Advanced tabs; drop unused JSON fields
- Remove the Mux/Sockopt/XHTTP raw-JSON fields from the Host form: they were not
wired into link generation and the inbound's structured editors are inbound-
specific (not reusable). The DB columns + read schema + generated type stay, so
they can get proper editors later. (HostFormSchema drops them; HostRecordSchema
keeps them.)
- Reorganize tabs to Basic / Security / Advanced / Clash / Subscription scope:
Security holds the TLS/cert fields (security, sni, sni-overrides, alpn,
fingerprint, pins, verify-by-name, ech); Advanced now holds the transport
overrides (host header, path).
- i18n: add pages.hosts.sections.security; drop the 3 unused field labels.
Gate: npm run typecheck + lint + test (556) + build green.
* style(ui): restore Mux/Sockopt/XHTTP fields in the Host Advanced tab
Put the three free-JSON override fields back, in the Advanced tab next to host
header / path (as JSON inputs — the inbound's structured editors aren't reusable
here). Re-added to HostFormSchema + defaults + the i18n labels.
Gate: npm run typecheck + lint + test (556) + build green.
* feat(hosts): add allowInsecure (rendered) + serverDescription/mihomoX25519/vlessRouteId fields
Closes most of the Remnawave-host gap analysis.
- model.Host: + allowInsecure, serverDescription (≤64), vlessRouteId (0-65535),
mihomoX25519. Auto-migrated (SQLite + Postgres verified); openapi regenerated.
- allowInsecure is fully RENDERED into subscription output (TDD):
- raw link: allowInsecure=1 (TLS/Reality, skipped for none) via the endpoint
builder;
- JSON/Clash: applyExternalProxyTLSToStream writes tlsSettings.settings.
allowInsecure, and clash applySecurity now emits skip-cert-verify for the tls
case (it previously only did so for Hysteria — a pre-existing gap, so inbound
allowInsecure now renders for vless/trojan/ss clash too).
- Frontend: the four fields added to the Host form (allowInsecure → Security,
serverDescription → Basic, vlessRouteId → Advanced, mihomoX25519 → Clash);
serverDescription shown under the remark in the list. Schema + i18n updated.
serverDescription / vlessRouteId / mihomoX25519 are stored + editable; their
deeper rendering (and per-host mux/sockopt/xhttp into JSON/Clash, plus a per-host
xray JSON template) are tracked as follow-ups.
Gate: go test ./... green (SQLite + Postgres for the host schema/migration);
go build linux+windows; go vet + gofmt clean; npm run gen + typecheck + lint +
test (556) + build green; generated files in sync.
* feat(sub): render host sockopt + xhttp-extra params into JSON/Clash output (TDD)
A host's sockoptParams and xhttpExtraParams (free-JSON) now take effect:
applyHostStreamOverrides injects sockopt into the per-host stream (re-added since
the base stream strips it) and merges xhttpExtraParams into xhttpSettings, called
in both getConfig (JSON) and getProxies (Clash) right after the per-host TLS
apply. No-op for legacy externalProxy entries (keys absent) — characterization
snapshots unchanged.
mux rendering is outbound-level (overrides outbound.Mux) and needs a genVless/
genVnext/genServer signature change — deferred, along with the per-host xray
JSON template.
Gate: go test ./internal/sub/... + go test ./... green (snapshots unchanged);
go build + vet + gofmt clean.
* feat(sub): render host muxParams as a per-host JSON outbound mux override (TDD)
genVnext/genVless/genServer take a muxOverride: a host's muxParams (when valid
JSON) overrides the global mux on its JSON outbound; empty falls back to the
panel mux (behavior unchanged for non-host configs). Completes the host
mux/sockopt/xhttp trio. Test call sites updated for the new signature.
Gate: go test ./internal/sub/... + go test ./... green (snapshots unchanged);
go build + gofmt clean.
* style(ui): show Host security fields conditionally per security (like externalProxy)
* feat(sub): apply host SNI + fingerprint override for reality (TDD)
A reality host now overrides SNI and fingerprint while inheriting publicKey/
shortId from the inbound (reality keys can't be host-supplied). Previously the
reality link kept the inbound's serverName because the TLS appliers are gated to
security=="tls".
- raw: applyEndpointRealityParams sets sni/fp on the params for reality;
- JSON/Clash: applyHostStreamOverrides sets realitySettings.serverName +
serverNames from the host SNI.
Gated to host endpoints via an isHost marker on the synthesized ep, so the legacy
externalProxy path stays byte-identical (characterization snapshots unchanged).
The marker is internal and never emitted.
Gate: go test ./internal/sub/... + go test ./... green; go build + vet + gofmt clean.
* fix(ui): start the Host inbound select unselected instead of showing 0
A new host left inboundId defaulting to 0, so the Select rendered "0". inboundId
is now optional in the form (undefined until chosen), so it shows its
placeholder ("Select an inbound"); the required rule still enforces a choice on
save. Port keeps 0 (means "inherit the inbound's port").
Gate: npm run typecheck + lint + build green.
* fix(ui): drop redundant :port suffix from the Host inbound select label
The inbound tag (e.g. in-59303-tcp) already carries the port, so the appended
":59303" was duplicated. Show just the remark/tag.
Gate: npm run typecheck + lint + build green.
* style(ui): apply the shared card hover shadows to the Hosts page
page-cards.css scoped its card styling + hover shadows to each page class but
not .hosts-page, so Hosts fell back to antd's default hoverable (a larger/blurry
shadow + pointer cursor). Add a .hosts-page block matching the other pages.
Gate: npm run build green.
* feat(hosts): move Tags to Basic tab, add Nodes field, accept VLESS route ranges
- Move the Tags field into the Host form's Basic tab and add a Nodes
multi-select (visual-only assignment, backed by the existing node_guids
column) so the Basic tab matches the reference layout.
- Replace the single-port vlessRouteId integer with a free-form vlessRoute
string that accepts comma-separated ports/ranges (e.g. 53,443,1000-2000);
format-validated on the frontend, stored verbatim on the backend.
- Regenerated frontend types/openapi from the changed model.
* feat(hosts): structured editors for Mux/Sockopt/XHTTP + new Final Mask
Replace the raw JSON textareas in the Host form's Advanced tab with the same
structured editors used elsewhere, under a nested tabbed layout (General / Mux /
Sockopt / XHTTP / Final Mask), mirroring the Sub-JSON settings tab:
- Mux: the Sub-JSON mux editor (enable + concurrency/xudpConcurrency/xudp443).
- Sockopt + XHTTP: reuse the outbound SockoptForm / XhttpForm, wrapped in an
isolated form that serializes the edited subtree back to the host's JSON
string (pruned so the override stays sparse).
- Final Mask: new host field (model + column + JSON-render wiring that merges
the masks into the host's JSON-subscription stream), edited via the shared
FinalMaskForm like the Sub-JSON Final Mask editor.
Each editor stays a controlled value/onChange component bound to its existing
host JSON string field; backend rendering of mux/sockopt/xhttp is unchanged.
* feat(hosts): drop XHTTP + Xray-JSON-template overrides; fix mobile form layout
Remove the host's XHTTP extra-params and Xray-JSON-template overrides entirely
(model fields + columns, JSON-subscription render paths incl. hostTemplateOutbound,
schema, form tab/field, i18n, openapi codegen, and their tests) — they did not
fit the host model. Mux, Sockopt and Final Mask stay as structured editors.
Mobile fixes for the Edit Host modal:
- responsive width (95vw on mobile, was a fixed 760px that overflowed the
viewport and clipped the tabs/labels) + a scrollable body so the footer stays
on screen;
- Mux fields use responsive Row/Col (stack on mobile) instead of a fixed-width
label grid.
* fix(hosts): hide the spurious horizontal scrollbar in the Edit Host modal
Setting overflowY:auto on the modal body forced overflow-x to auto too (CSS
rule), so antd Row's negative gutter margins triggered a horizontal scrollbar.
Pin overflowX:hidden.
* feat(hosts): inbound-style responsive field layout + icon empty state
- Host form (main form + Mux/Sockopt/Final Mask editors) now use the inbound
form's label layout: label beside the input on desktop (labelCol sm span 8 /
wrapperCol sm span 14, right-aligned), stacked label-above-input on mobile.
Rewrote HostMuxForm onto an internal antd Form so it follows the same layout
instead of a manual grid.
- Empty hosts table now shows the host icon + the shared 'Nothing here yet'
(noData) text, matching Nodes/Inbounds/Clients, replacing the bespoke
'No hosts yet…' string.
* fix(hosts): avoid nested <form> in the Edit Host modal
The Mux/Sockopt/Final Mask editors each render their own antd Form inside the
host's main Form, producing an invalid nested <form> DOM node (hydration
warning). Render those inner forms with component={false} so they keep the form
instance/context but emit no <form> element.
* fix(hosts): make the Mux enable toggle work
The Switch's checked state came from Form.useWatch('mux'), but the mux object
field had no registered Form.Item while disabled, so setFieldValue never
notified the watcher and the toggle stayed off. Bind the Switch to a real
name='enabled' field (antd drives its checked state directly) and keep the
sub-fields registered via hidden={!enabled}, serialized to the flat mux JSON.
* refactor(hosts): reuse the outbound MuxForm instead of a bespoke Mux editor
The Mux fields duplicated the outbound MuxForm. Reuse it through the same
wrapper as Sockopt: generalize OutboundSubtreeJsonForm with defaultSubtree
(pre-fill on enable) and a serialize hook, and have HostMuxForm render MuxForm
at the ['mux'] path. The host keeps its inherit-when-off semantics by storing ''
unless mux.enabled. Also drops the now-unused enableSwitch path from the
wrapper (only the removed XHTTP editor used it).
* style(hosts): use default-width Port input like the inbound form
The host Port used width:100% (full width); the inbound's numeric inputs use
antd's default width. Drop the override so Port matches. The Mux number inputs
already use the default width via the reused MuxForm.
* refactor(sockopt): readable customSockopt editor as a shared component
The customSockopt rows were a single cramped Space.Compact line and duplicated
verbatim in the inbound and outbound sockopt forms. Extract a shared
CustomSockoptList that renders each entry as a titled group of labeled fields
(System / Level / Opt / Type / Value), matching the rest of the form, and use it
in both (and thus the host Sockopt editor).
* fix(finalmask): drop the empty Custom Tables tag on a new sudoku mask
The sudoku TCP-mask default seeded customTables: [''] (one empty string), which
rendered as a blank removable tag. Seed [] instead.
* fix(sockopt): make the outbound (and host) Sockopt client-only
Per the XTLS sockopt docs, tproxy / acceptProxyProtocol / V6Only /
trustedXForwardedFor only apply to an inbound (listening socket); they are
meaningless on an outbound/dialer. Drop them from the outbound SockoptForm
(which the host reuses). The Sockopt default object still seeds those keys, so
the host also strips them on serialize, keeping its override honest to the
server/client split. The inbound SockoptForm is left unchanged.
* fix(sockopt): make the inbound Sockopt server-only
Complete the server/client split: drop the outbound/dialer-only fields from the
inbound SockoptForm — dialerProxy, domainStrategy, interface, addressPortStrategy,
happyEyeballs, tcpMptcp (client-only since Go 1.24 auto-enables MPTCP on listen).
mark stays (xray applies SO_MARK on inbound sockets too). Update the form-blocks
snapshot to the server-side field set (intentional spec change).
* feat(hosts): populate Sockopt dialerProxy with the panel's outbound tags
The host Sockopt editor reused the outbound SockoptForm with outboundTags=[],
so the dialerProxy dropdown was empty. Feed it the panel's outbound tags via
the existing useOutboundTags hook (shares the cached xray-config query;
blackhole excluded), so a host can chain through a subscription outbound by tag.
* fix(hosts): empty-state styling on direct load + exclude balancers from dialerProxy
- .card-empty was only defined in lazily-loaded Clients/Inbounds/Nodes
stylesheets, so a direct /hosts refresh rendered the empty table state
unstyled (faint + uncentered) until another page was visited. Re-declare it
in HostList.css so it's correct on first load.
- The Sockopt dialerProxy dropdown listed balancer tags (useOutboundTags merges
them in for mtproto egress). dialerProxy chains a single outbound, so balancers
aren't valid — switch to useOutboundTagGroups and use only the outbound group.
* fix(outbounds): icon + 'Nothing here yet' empty state; stop fading other pages
The Outbounds empty state was a faint '—', and OutboundsTab.css set the global
.card-empty to opacity:0.4 — which leaked onto whichever page's empty state was
shown after the Outbounds CSS had loaded (e.g. Hosts went faint after visiting
Outbounds). Render the icon + noData ('Nothing here yet') like the other lists,
and align .card-empty to the shared centered/secondary style (no opacity).
* fix(outbounds): custom empty state on the desktop table too
The desktop Outbounds Table had no locale.emptyText, so it showed antd's
default 'No data' box. Add the same ExportOutlined + noData empty state as the
card (mobile) view.
* style(sidebar): use ExportOutlined for the Outbounds nav item
The Outbounds sidebar item used UploadOutlined (an upload tray). Switch to
ExportOutlined, matching the outbound icon now used in the routing target and
the outbounds empty states.
* feat(hosts): icons on the form tabs (icon-only on mobile)
Wrap every Host form tab label (Basic/Security/Advanced/Clash/Subscription
scope and the nested General/Mux/Sockopt/Final Mask) with catTabLabel, so the
tabs show icon + text on desktop and just the icon (with a tooltip) on mobile,
matching the Settings/Xray tab bars.
* refactor(hosts): fold Exclude-from-formats into Advanced, drop the one-field tab
The Subscription scope tab held only excludeFromSubTypes after Tags moved to
Basic — a niche per-format scoping knob. Move it into the Advanced > General
sub-tab and remove the standalone tab (and its now-unused subScope label/icon).
* feat(sub): per-client remark template variables; drop the remark model & Show Usage Info
* fix(migration): cap seeded host remark at the model's 256-char limit, not 40
|
||
|
|
d882d6aa74 |
feat(inbounds): add Real client IP presets to capture visitor IP behind CDN/relay
Surface the existing sockopt knobs (acceptProxyProtocol, trustedXForwardedFor) as a guided 'Real client IP' preset selector in the inbound form, so the real visitor IP is recovered behind Cloudflare CDN or an L4 tunnel/relay instead of recording the intermediary address. Presets are mutually exclusive, warn on incompatible transports, and add tooltips, docs, and translations for all locales. |
||
|
|
c1fbfd0510 |
fix(outbound): parse xmux from imported share links (#5353)
The inbound link generator bundles xmux and downloadSettings as nested objects inside the `extra=` JSON blob, but the outbound link parser only pulled scalar fields and headers from it, silently dropping xmux on import. Extract the nested objects too so they round-trip into the outbound XMUX sub-form. |
||
|
|
f00512d12e |
fix(frontend): TProxy schema, VLESS+XHTTP flow links, clearable Jalali date picker (#5339, #5322, #5313)
- #5339: accept transportless tunnel/TProxy streamSettings that carry no `security` key by adding a transportless branch to SecuritySettingsSchema, mirroring NetworkSettingsSchema. Fixes "streamSettings.security Invalid input". - #5322: emit XTLS Vision `flow` in panel VLESS share links for XHTTP+vlessenc via the shared canEnableTlsFlow predicate, so panel links match the form and the subscription output. - #5313: give the Jalali expiry date picker a working clear (X) button (remount on clear, since the library reads `value` only on mount) and a blank placeholder instead of the library's hardcoded Persian text. |
||
|
|
cdaf5f80db |
fix(inbound): strip XHTTP client-only fields from xray config, keep for subscriptions (#5349)
Inbound XMUX and other client-side xHTTP knobs were written into bin/config.json even though xray-core's server listener ignores them. Strip them in GenXrayInboundConfig while leaving the DB row intact so buildXhttpExtra still pushes defaults to clients via share links. |
||
|
|
7605902324 |
Test-quality audit: fix 2 prod bugs, strengthen weak tests, add mutation/fuzz/CI tooling (#5345)
* test(audit): add gremlins/rapid/coverage tooling + AUDIT.md scaffold * test(audit): hygiene sweep (race-clean except logger global; Finding #2) + smell inventory * test(audit): cover untested error/edge branches (TLS proxy+pin, migration tag cleanup=Finding #1) * test(audit): strengthen internal/sub link tests (dedup key, TLS/Reality mapping, clash well-formedness) * test(audit): property (rapid) + fuzz tests for joinHostPort/userinfo/pin/ParseLink * test(audit): tighten frontend subSortIndex rejection assertions + wire coverage * ci(audit): add shuffle gate + non-blocking race job (Finding #2) + fuzz-smoke; document mutation policy * chore(audit): gitignore frontend coverage output * test(audit): exhaustive whole-repo pass — strengthen 5 weak/fake tests (netproxy, CSP, modal per-protocol loops, schema coercions) * docs(contributing): add Testing section (conventions, race/shuffle, fuzz, mutation policy); drop AUDIT.md ledger * fix(logger,migration): guard logBuffer with mutex; execute legacy tag cleanup (tx.Exec); make CI race gate blocking * ci(mutation): add nightly scoped gremlins workflow (informational artifacts) * test(audit): strengthen runtime tests — baseURL scheme/port bounds, isNonEmptySlice, trafficReset * test(audit): strengthen clash tests — reality field mapping + tcp-header validation * test(audit): runtime — egress-proxy + content-type tests; drop redundant bp=='' branch * test(audit): strengthen link parser/helper tests (defaultPort, splitComma, base64, canonicalQuery, tls/reality/transport mapping) * test(audit): strengthen sub/xray/common/netsafe/mtproto/config/middleware tests (kill surviving mutants) * test(audit): raise timeout on protocol-iteration modal tests (heavy re-renders, slow on CI) * fix(logger): GetLogs returns at most c entries (off-by-one fix; addresses PR review) * perf(logger): snapshot logBuffer under lock so GetLogs doesn't block logging; clarify fuzz-seed docs (addresses PR review) |
||
|
|
66a9a788fc |
fix(reality): load dest as target alias so existing inbounds aren't wiped (#5295)
xray-core accepts both `target` and `dest` for the REALITY destination (infra/conf/transport_internet.go: REALITYConfig has json:"target" and json:"dest"). The frontend schema only knows `target`, so an inbound whose realitySettings use `dest` — older panel builds, external tools, or the panel's own /panel/api/inbounds API — loads with an empty (required) Target field even though xray is running fine. Re-saving then serializes the blank `target` and drops the working `dest`, breaking REALITY on the next restart. Normalize `dest` -> `target` on parse (z.preprocess) when `target` is absent/empty, matching xray-core's alias behavior. Add unit tests covering the schema directly and through the security discriminated union. Co-authored-by: Volov <volovdata@google.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> |
||
|
|
7c737820d1 |
fix(links): bracket ipv6 hosts in share links and qr codes (#5310)
* fix(sub): bracket ipv6 hosts in share links * fix(frontend): bracket ipv6 hosts in share links |
||
|
|
f4bbaf40f0 |
feat(ui): show per-inbound live speed (#5261)
* feat(utils): add speedFormat utility and tests * feat(inbounds): add InboundSpeedEntry type * feat(inbounds): add speed column to inbound list * feat(inbounds): show speed in inbound stats modal * feat(inbounds): compute inbound speed from traffic deltas * feat(inbounds): wire inbound speed through page * feat(i18n): add speed translation for all locales * refactor(inbounds): dedupe live-speed UI and harden formatting Extract a shared InboundSpeedTag component and isActiveSpeed guard used by the speed column and stats modal, unify InboundSpeedEntry into a single type, and route speedFormat through sizeFormat. Also guard sizeFormat against non-finite input (no more "NaN PB/s") and clear stale per-inbound speeds when a traffic poll returns no deltas. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
90e6217749 | fix(inbound): preserve custom share strategy on edit (#5225) | ||
|
|
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>
|
||
|
|
f1a4286e2f |
feat(sub): per-inbound sort order for subscription links
Add a subSortIndex field to inbounds that controls the order of links in subscription output only: the raw sub body, the HTML sub page, and the JSON/Clash formats (all served from the same query). Lower values come first; ties keep id order. The panel inbound list is unaffected. The value is editable in the inbound form next to the share-address fields, propagates to nodes via wireInbound, and follows the usual node-sync rules (copied on import, mirrored while not dirty, never a structural change). Rescoped from #5214 by @Ponywka. |
||
|
|
60da6bed15 |
fix(xhttp): stop injecting scMaxEachPostBytes/scMinPostsIntervalMs defaults (#5141)
The panel seeded xhttp configs with scMaxEachPostBytes=1000000 and scMinPostsIntervalMs=30 — xray-core''s own defaults — and emitted them into every generated config and share link. The literal scMinPostsIntervalMs=30 is a stable DPI fingerprint that Russia''s TSPU keys on to block connections on mobile networks. New configs no longer seed these values (empty schema/template defaults, so xray-core applies its internal defaults). For configs already stored with the old defaults, the link/subscription builders now drop values equal to xray-core''s defaults instead of advertising them — covering panel share links, the raw subscription, and the JSON subscription without requiring every inbound to be re-saved. Non-default values the user set deliberately are still emitted. |
||
|
|
2a7342baa9 |
feat: add inbound share address strategy (#5162)
* feat: add inbound share address strategy Allow node-managed inbounds to choose whether exported share links use the node address, routable listen address, or a custom endpoint. Preserve locally configured share address fields during remote node traffic sync. Refs #5161 Refs #4891 * fix: preserve inbound share address settings Forward share address fields to remote nodes, keep existing values when older update payloads omit them, align localhost handling between frontend and subscriptions, and preserve share address settings when cloning inbounds. * fix: keep share address strategy out of subscriptions Limit the new share address strategy to direct exported share links and QR codes. Restore subscription address resolution to the existing panel-owned behavior and update the UI help text accordingly. * fix: address share address review feedback * fix: validate custom share address * fix --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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 |
||
|
|
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). |
||
|
|
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> |
||
|
|
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 | ||
|
|
a014c01725 |
feat(api-docs): generate OpenAPI components/schemas from Go structs
A new emit_jsonschema.go walks the same allow-listed structs as the zod/types/examples emitters and writes generated/schemas.ts (SCHEMAS). build-openapi mounts it under components.schemas and points each typed response obj at a $ref instead of an untyped {} blob, so Swagger renders real models and openapi-generator can emit clients.
Also add a vitest guard that safeParses every EXAMPLES entry against its generated zod schema, reviving the previously unused generated/zod.ts and catching drift between the example and schema emitters.
|
||
|
|
6ed6f57b5c |
fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target (#4988)
* fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target Strip mode-specific XHTTP fields for stream-one, reset harmful sockopt defaults to 0, split server/client Reality fields on save, validate target host:port in the inbound form, and expose Happy Eyeballs for the direct freedom outbound. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(panel): keep REALITY public key on the wire, guard freedom noises The REALITY server/client wire split deleted realitySettings.settings on save, but the panel stores the REALITY public key there and every share-link / subscription generator reads it back from that path (frontend inbound-link.ts, Go subService/subJsonService/subClashService). Stripping it produced empty pbk= links, breaking client connectivity after save+reload. Revert the reality normalization (drop normalizeRealityForWire and the key sets), restore the inbound REALITY form fields (uTLS, spiderX, publicKey, mldsa65Verify) while keeping the new validated target field, and restore the mldsa65Verify clear handler. Also guard freedomToWire against undefined noises/finalRules (same defensive treatment as the existing fragment guard, issue #4686) which the new freedom-outbound test surfaced as a crash. Tests now assert the public key is preserved. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com> |
||
|
|
e7ffae5329 |
fix(outbound): import ech and pcs from TLS share links
The vless/trojan link parser's TLS branch read only sni/fp/alpn, so the ech (echConfigList) and pcs (pinnedPeerCertSha256) query params were dropped on import even though buildStream allocates both fields. Read them in applySecurityParams to match the inbound link generator and the hysteria2 parser. |
||
|
|
1a64d7e9de |
feat(tls): add ocspStapling to certificate config
Expose the OCSP Stapling refresh interval (seconds) on the TLS certificate object in the inbound security form, defaulting to 3600s to match xray-core. Covers both file-backed and inline cert shapes. |
||
|
|
db5ce06256 |
fix(panel-proxy): route custom geo and http(s) Telegram through panelProxy
Custom geosite/geoip downloads built their own ssrfSafeTransport and never used the configured Panel Network Proxy, so geo updates failed on servers where GitHub is filtered. Route all custom-geo HTTP (startup probes + downloads) through panelProxy when set, falling back to the direct SSRF-guarded transport otherwise; the target URL stays SSRF-validated. The Telegram bot only honored a socks5:// panel proxy and silently rejected http(s)://, despite the setting advertising both. Branch the fasthttp dialer (FasthttpHTTPDialer for http(s), FasthttpSocksDialer for socks5) and accept all three schemes in the fallback and NewBot validation. Add tests proving the panel proxy is used by custom geo and that the bot dialer speaks HTTP CONNECT vs SOCKS5 per scheme. |
||
|
|
e7c11c913a |
feat(inbounds): per-proxy Pinned Peer Cert SHA-256 + labeled External Proxy form
Redesign the Add Inbound -> Stream External Proxy section into labeled per-entry cards (Force TLS / Host / Port / Remark and, under TLS, SNI / Fingerprint / ALPN) and add a Pinned Peer Cert SHA-256 field with a generate-random-hash button to each entry. The pin flows end to end into share links: pcs for vmess/vless/trojan/ss (stripped when a proxy forces security off) and the hex-normalized pinSHA256 for Hysteria. JSON and Clash subscriptions emit the native pinnedPeerCertSha256 / pin-sha256 via the cloned stream. Adds the forceTls label across all 13 locales plus frontend and Go tests. |
||
|
|
6ee462ac8e |
fix(links): use configured domain for panel copy/QR links on loopback
The panel's copy/QR share links are built client-side and fell back to window.location.hostname, so reaching the panel over an SSH tunnel (127.0.0.1/localhost) leaked localhost into the links - unlike the backend subscription path, which falls back to the configured Sub/Web Domain (issue #4829). Expose webDomain/subDomain via /defaultSettings and add preferPublicHost: when the browser host is loopback, prefer the configured Sub Domain (then Web Domain) for share/QR links. An explicit node override or per-inbound listen still wins; a routable browser host is kept as-is. Closes #4829 |
||
|
|
ac67c52278 |
fix(hysteria2): emit pinSHA256 as hex in subscriptions, not base64
Hysteria2 clients backed by Xray-core hex-decode the pinSHA256 URI param and crash on the base64 value the panel stores for pinnedPeerCertSha256 (xray-core native TLS format). Normalize each pin to bare lowercase hex when building the Hysteria link, accepting base64, bare hex, and colon-separated openssl fingerprints; values that are neither are passed through untouched. Applied in both the backend subscription generator and the frontend link builder. The pcs share-link and JSON-sub paths keep base64 for their consumers. Fixes #4818. |