Commit Graph

149 Commits

Author SHA1 Message Date
Rouzbeh† 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>
2026-06-23 14:55:12 +02:00
MHSanaei 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
2026-06-23 13:24:16 +02:00
Sanaei adc64bb804 fix(nodes): cloned-node attribution, node-hosted client display (online/speed/counts), and sync robustness (#5488)
* fix(nodes): keep cloned nodes (shared panelGuid) in separate attribution buckets

#4983 keys online/inbound attribution by panelGuid, assuming it is globally unique. Cloned node servers ship an identical panelGuid in their copied settings, so the master collapsed several physical nodes into one bucket: GetMergedNodeTrees merged their online sets under one key and every inbound on those nodes (same origin_node_guid) read that merged set, so the inbound page showed online cross-attributed and counts inflated.

Fall back to the node-unique synthNodeGuid(node.Id) whenever a node's panelGuid is shared by another of the master's direct nodes. Applied consistently at originGuidFor (origin_node_guid write), the online-tree key plus a self-key remap for nodes that report a GUID-keyed tree, effectiveNodeGuid, and recountByGuid's inbound bucketing. sharedNodeGuids computes the collision set. Online now works without node changes; making panelGuids unique restores real-GUID identity and also fixes GUID-keyed IP attribution.

* fix(nodes): extend duplicate-GUID hardening to master collisions, IP attribution, and a heartbeat warning

Builds on the node-vs-node fix: a node's GUID is now also treated as ambiguous when it equals the master's own panelGuid (a node cloned from the master), so the master's local clients and that node can't merge. Centralized as ambiguousNodeGuids(nodes, selfGuid) + effectiveNodeKey(node).

Applied the same node-unique fallback to the GUID-keyed IP attribution that #4983 added but the prior commit left collapsing: MergeClientIpsByGuid remaps a cloned node's own subtree to its node-unique key, nodeGuidNameMap resolves names by that key, and node deletion purges both keys. Added a throttled heartbeat warning so the operator is told to regenerate a duplicate panelGuid. Tests cover master-collision, effectiveNodeKey, and the IP remap.

* fix(node-sync): log the client-IP-attribution 404 once per node, not every cycle

Old-build nodes lack panel/api/clients/clientIpsByGuid and answer 404 on every IP-sync cycle (~10s), which floods the debug log now that the IP phase actually runs. Note the missing endpoint once per node (re-armed if the node later recovers or is upgraded) and keep logging genuine fetch errors.

* fix(nodes): remap a cloned node's own-panelGuid origin so the inbound page shows online

These nodes report their OWN inbounds with their own panelGuid as OriginNodeGuid, so originGuidFor returned the shared GUID verbatim and never remapped it. origin_node_guid stayed the shared GUID while online was keyed under the node-unique key, so the inbound page (which reads the stored origin_node_guid) looked up an empty bucket and showed everyone offline — even though the Nodes page (which derives the key live) was correct. Treat an origin equal to the node's own panelGuid as the node's own inbound and resolve it through selfKey; keep only a genuinely different (descendant) origin across hops.

* fix(node-sync): don't delete a node's central inbounds when its snapshot is empty

The central-inbound sweep deletes any central inbound whose tag is absent from the node's snapshot, with no guard for an empty snapshot. A node mid-restart or with a transient DB error (e.g. Postgres 57P01) can return an empty inbound list with success=true, which wiped all of that node's central inbounds and their clients (and reset traffic history on re-create) — observed on the Germany node: 0 clients but still 44 online (online survives because it comes from the snapshot's online tree, not the central inbound). Skip the sweep entirely when the snapshot reports zero inbounds; a real per-inbound deletion still sweeps via a non-empty snapshot that omits one tag.

* fix(email): stay silent when SMTP notifications are disabled

The event subscriber is registered unconditionally and only checked the per-event list (smtpEnabledEvents, default login.attempt,cpu.high) — not the smtpEnable master toggle. Login events are always published, so a panel with smtpEnable=false still attempted a send on every login and logged 'email subscriber: send failed: smtp host not configured'. Gate HandleEvent on GetSmtpEnable() so a disabled-SMTP panel does nothing, matching the comment where the subscriber is registered.

* fix(nodes): count only expired/exhausted as 'ended', not disabled clients

The per-node depleted (ended) count folded disabled clients in with expired/exhausted (expired || exhausted || !Enable), so the Nodes page 'ended' chip was inflated and inconsistent with the inbound page, where disabled and depleted are separate buckets. Count only expired/exhausted in both GetAll and recountByGuid so 'ended' means the same thing on both pages.

* feat(nodes): show live speed for node-hosted inbounds

Inbound speed is computed on the dashboard from a 'traffics' delta feed, which only the local Xray poll produced — so node-hosted inbounds showed no speed. The node sync now diffs successive per-inbound cumulative totals (it polls @5s, same as the local poll) and broadcasts the byte deltas as a separate 'nodeTraffics' field, keyed by the central tag the dashboard already matches. The frontend applies 'traffics' to local inbounds and 'nodeTraffics' to node inbounds within their own scope, so the two 5s polls don't clobber each other and idle inbounds still clear. Deltas clamp to 0 on a reset; a node that fails to sync keeps a stale total so its delta is 0 (no phantom speed).

* fix(nodes): normalize node-inbound speed by elapsed time to avoid recovery spikes

Adversarial review found that a node's cumulative inbound counter keeps climbing while the master can't reach it, so the first delta after a gap (node outage, skipped poll, slow node) spans more than one 5s window but was still divided by the dashboard's fixed 5s — rendering an impossible one-tick speed spike on recovery (and a 2x over-report after a skipped poll). Now each delta is normalized to the fixed window using the real elapsed time since the inbound's counter last changed, so a backlog shows the true average rate over the gap. The change timestamp advances only on actual movement, so idle stretches average correctly when traffic resumes; resets rebaseline. Also moves the maybePushGlobals doc comment back onto its function.

* fix(inbounds): keep last speed across page navigation instead of blanking

Speed is delta-derived, so it can't be recomputed until the first poll after mount. The websocket subscription and speed state are page-scoped (useWebSocket lives in InboundsPage), so leaving to another page and returning blanked the Speed column for up to one 5s poll. Cache the last speed map across mounts (module scope, 15s recency guard) and seed the state from it, so returning shows the last throughput immediately and the next poll refreshes it. Applies to both local and node-hosted inbound speed.

* fix(inbounds): rebalance table column widths so it fills width without gaps

Inbound list columns had small fixed widths summing far below the table's
full width, so AntD spread the leftover space evenly into wide empty gaps.
Widen the content-heavy columns (protocol, clients, traffic, node) so the
slack lands there, keep the small ones (id, port, enable) tight, and make
scroll.x track the visible columns' total so the table never collapses
below content and adapts when conditional columns are hidden.

* feat(nodes): show active/disabled client counts on the nodes page like inbounds

The nodes page only showed total/online/ended, and (since ended now excludes disabled) disabled clients were invisible there. Compute per-node active and disabled counts — in both GetAll and recountByGuid, with the same depleted-wins-over-disabled precedence the inbound page uses so the buckets stay mutually exclusive — and render total/active/disabled/ended/online chips matching the inbound page (table column + mobile stats modal).

* fix(nodes): count active/disabled/ended by client email, not stale inbound_id

The per-node client breakdown filtered client_traffics by inbound_id, but that column goes stale after an inbound is delete+recreated (e.g. the Germany node), so almost every traffic row pointed at a dead inbound id and the counts collapsed — active showed ~5 instead of ~1100. Classify each node client via client_inbounds -> clients joined to client_traffics by EMAIL (the reliable key), deduped per node/guid, in both GetAll and recountByGuid. Now active/disabled/ended on the nodes page match the inbound page. Added a regression test that proves matching works with a deliberately stale inbound_id.

* style(nodes): widen Clients column so the count chips fit one tidy line

After adding the active/disabled chips, the 5 chips (total/active/disabled/ended/online) no longer fit the 160px Clients column and wrapped to two lines. Widen it to 220 and drop the Space wrap so they render on a single line like the inbound page, and zero the total tag's margin for even spacing. Same principle as 79ff283 (give the content column enough width).

* style(nodes): tighten Clients chip spacing to match the inbound page

AntD's default tag side-padding (~8px) put a wide gap between the count chips. Apply the inbound page's compact padding ('0 2px') + client-count-tag (tabular-nums) to each chip and narrow the column to 180 so the numbers sit close together like the inbound list instead of floating apart.
2026-06-22 20:20:55 +02:00
Sanaei 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.
2026-06-22 00:22:28 +02:00
MHSanaei 03e89683dd fix(tls): ping the inbound's own port for remote cert pinning
The pin-from-remote button passed only the SNI to 'xray tls ping', which defaults to :443 — so it never reached a self-hosted inbound on another port and failed with a vague 'no certificate hash found'. Append the inbound's port when the SNI carries none, and surface the underlying ping failure (dial refused, timeout) in the error.
2026-06-21 19:27:37 +02:00
MHSanaei 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.
2026-06-21 19:15:57 +02:00
MHSanaei 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.
2026-06-21 15:58:42 +02:00
w3struk 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>
2026-06-20 00:57:47 +02:00
MHSanaei 4915d6b18d refactor(frontend): move form-item hints from extra to tooltip
Switch reality target, node options, and WARP auto-update-IP hints from
inline extra text to label tooltips for a cleaner form layout.
2026-06-17 17:24:16 +02:00
Sanaei 709b332d17 feat(hosts): managed Hosts for per-host subscription link overrides (#5409)
* test(sub): characterize current link output (externalProxy + single-link baselines)

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

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

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

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

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

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

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

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

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

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

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

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

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

Phase 3 of the Hosts feature.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Phase 6 of the Hosts feature — the admin UI.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes most of the Remnawave-host gap analysis.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Gate: npm run typecheck + lint + build green.

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

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

Gate: npm run typecheck + lint + build green.

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

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

Gate: npm run build green.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(migration): cap seeded host remark at the model's 256-char limit, not 40
2026-06-17 12:06:55 +02:00
MHSanaei 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.
2026-06-15 23:50:04 +02:00
MHSanaei bbab83db17 refactor(frontend): stack client credential fields and use label hints on inbound form
Stack UUID/password/subId/auth/flow/security fields vertically in the client modal instead of two-column rows, and replace the inbound form's 'extra' help lines with hover tooltip hints on field labels.
2026-06-15 21:38:11 +02:00
tonymoses10 b5872af279 Frontend operation button size optimization (#5343)
* Update ClientsPage.tsx

* Update GroupsPage.tsx

* Update RowActions.tsx

* Update NodeList.tsx

* Update BalancersTab.tsx
2026-06-15 14:26:51 +02:00
Nikan Zeyaei f4bbaf40f0 feat(ui): show per-inbound live speed (#5261)
* feat(utils): add speedFormat utility and tests

* feat(inbounds): add InboundSpeedEntry type

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

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

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

* feat(inbounds): wire inbound speed through page

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

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

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

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

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-14 22:39:40 +02:00
MHSanaei 0f7da02a07 style(inbounds): show total up/down with directional arrows
Replace the ambiguous swap icon on the total traffic statistic with
explicit up/down arrows next to each value.
2026-06-12 19:19:42 +02:00
iYuan 90e6217749 fix(inbound): preserve custom share strategy on edit (#5225) 2026-06-12 18:38:01 +02:00
MHSanaei 6e20588236 style(ui): enlarge row action icons and rebalance clients table widths
Bump row action icons to 18px across the clients, inbounds, groups and
nodes tables for better visibility.

In the clients table, cap the Client column at 220px and give Duration a
fixed width so the Traffic column becomes the flexible one that absorbs
the remaining horizontal space instead of Client growing oversized.
2026-06-12 18:25:27 +02:00
MHSanaei 5eec178483 feat(mtproto): route Telegram egress through Xray routing rules
Add a per-inbound "Route through Xray" toggle (off by default) plus an
optional outbound picker on MTProto inbounds. mtg only supports a SOCKS5
upstream, so when enabled the panel injects a loopback SOCKS bridge into
the generated Xray config — tagged with the inbound's own tag — and mtg
dials Telegram through it via a [network] proxies upstream. The router
then governs Telegram egress: matchable in the Routing tab, or forced to a
chosen outbound/balancer via the picker.

- mtproto: Instance carries RouteThroughXray + XrayRoutePort (in the
  fingerprint); InstanceFromInbound parses them; renderConfig emits the
  socks5 [network] upstream; freeLocalPort exported as FreeLocalPort.
- xray.go: injectMtprotoEgress appends the loopback SOCKS bridge and
  prepends an optional inboundTag->outbound/balancer rule, hot-appliable
  like injectPanelEgress.
- inbound.go: backend-owned egress port persisted in settings, allocated
  once and carried across edits (stored value wins); stripped with the
  inert outboundTag when routing is off; allocation failure fails the save;
  routed add/update/del force a config regen.
- mtproto_job: skip folding mtg metrics for routed inbounds (the bridge,
  carrying the inbound tag, is metered by xray_traffic_job) to avoid
  double-counting.
- frontend: toggle + outbound/balancer Select (useOutboundTags) on the
  MTProto form; i18n keys for all locales.
2026-06-12 17:58:45 +02:00
MHSanaei 5af02265ec fix(inbound): remove stale mkcp-legacy finalmask when switching away from mKCP
Switching the transport to mKCP auto-seeds a mkcp-legacy entry into
finalmask.udp, but switching back to another transport only dropped the
kcpSettings blob and left the mask behind. It survived downstream pruning
(finalmask.udp was non-empty) and bled into every client share link.

Strip auto-seeded mkcp-legacy entries from finalmask.udp whenever the
network changes away from kcp, leaving user-authored masks intact.

Fixes #5221
2026-06-12 15:35:41 +02:00
MHSanaei 7c698c4bcf feat(inbound): support abstract unix sockets (@ prefix) in Address field
Accept the @-prefixed abstract socket form (e.g. @xray/in.sock) for an
inbound listen address, not just path-based sockets. The form now allows
Port 0 for both, and the Address help text documents the @ form across
all locales. The backend already treated both prefixes as unix sockets.
@
2026-06-12 14:34:02 +02:00
MHSanaei 0cefadd166 feat(ui): use CodeMirror editor for Import Inbound and Inbound JSON 2026-06-12 12:38:18 +02:00
Rouzbeh† 0766e16684 feat: implement inbound XMUX form fields (#5211)
* feat: implement inbound XMUX form fields

* fix: replace any cast to satisfy eslint

* test: update xhttp form snapshot for XMUX

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

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

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

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

---------

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

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

Rescoped from #5214 by @Ponywka.
2026-06-12 12:03:22 +02:00
MHSanaei 253063b785 feat: filter inbounds and clients by node (#4997)
Multi-node panels had no way to narrow the inbounds or clients lists to
a single node. Add a node filter to both pages:

- Inbounds: a toolbar select (All / Local / each node) that filters the
  list client-side; shown only when the panel has nodes or node-attached
  inbounds.
- Clients: a Nodes multi-select in the filter drawer. Node selections
  are mapped onto inbound IDs client-side and fed through the existing
  inbound CSV paging parameter, so the paging backend is untouched; an
  impossible id (-1) is sent when no inbound matches so the filter
  yields an honest empty result. InboundOption now carries nodeId to
  make the mapping possible.

The local panel is selectable via a 0 sentinel (inbounds without a
nodeId). New i18n keys in all 13 locales.
2026-06-12 09:33:35 +02:00
MHSanaei d04cb10971 feat(wireguard): per-peer comments for identifying devices (#5168)
WG peers were only identifiable by their keys. Add an optional panel-side
comment per peer: editable in the inbound form (echoed next to "Peer N"
in the section header), stored in the settings JSON alongside the
panel-only privateKey (xray-core ignores unknown peer fields), and
appended to the share link / .conf remark so the device is identifiable
in client apps too.
2026-06-12 09:10:57 +02:00
MHSanaei 10a0c9131c fix(hysteria): clamp udpIdleTimeout to xray-core's accepted 2-600s range (#5117)
The schema and form inputs allowed any value >= 1, but xray-core rejects
UdpIdleTimeout outside 2-600 seconds at startup, so an out-of-range value
silently killed the whole config.
2026-06-12 01:21:54 +02:00
MHSanaei a5e5640804 fix(inbound): explain how to unlock fallbacks on the inbound form (#5014)
The fallbacks card only renders for VLESS/Trojan over RAW with TLS or
Reality security, and a new inbound starts at security=none — so the Add
Inbound page looked like it had lost fallback support entirely. Show an
inline hint in that state pointing at the Security tab.
2026-06-12 01:21:38 +02:00
MHSanaei c47a905ad2 fix(inbound): offer node share-address strategy only when a node exists
The `node` share-address strategy resolves to an address only when the
inbound can live on a node; for a local inbound it is always empty and
behaves like `listen`. Drop the `node` option from the picker unless an
enabled, node-eligible node exists, and coerce the value to `listen`
otherwise so the Select never shows an option that does nothing.
2026-06-11 23:32:47 +02:00
iYuan 2a7342baa9 feat: add inbound share address strategy (#5162)
* feat: add inbound share address strategy

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

Refs #5161

Refs #4891

* fix: preserve inbound share address settings

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

* fix: keep share address strategy out of subscriptions

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

* fix: address share address review feedback

* fix: validate custom share address

* fix

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 20:24:15 +02:00
MHSanaei aeb2217ae5 fix(ui): classify ended clients as depleted, not disabled, on inbounds page
The auto-disable job flips client.enable off in the settings JSON when a
client expires or exhausts its traffic, so the inbounds-page rollup filed
every ended client under the gray Disabled badge (and double-counted it
in Depleted when stats were present). Classify with depleted-first
priority, matching computeClientsSummary and the client info modal.

Also backfill cross-inbound client_traffics rows in GetInboundsSlim:
the row is keyed on email and only preloads on the inbound the client
was created on, so on every other attached inbound the depleted/expiring
checks could never fire.
2026-06-11 14:05:02 +02:00
aleskxyz 8f408d2d6a feat(routing): show tag (remark) in routing rules list (#5151)
* feat(routing): show tag (remark) in routing rules list

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

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

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

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

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

---------

Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-11 12:46:24 +02:00
Rouzbeh† c7a76e9626 fix: enable XTLS vision flow for VLESS+XHTTP+vlessenc in UI and share links (#5157) (#5185)
* fix: enable XTLS vision flow for VLESS+XHTTP+vlessenc in UI and share links (#5157)

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

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

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

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

---------

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

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

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

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

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

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

---------

Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-06-11 11:05:42 +02:00
MHSanaei 3092326d9e refactor: replace custom geo manager with Xray-core native geodata auto-update
Remove the panel-side custom geo download feature (service, controller,
/panel/api/custom-geo/* endpoints, CustomGeoResource model, UI tab) in
favor of Xray-core's native geodata section
(https://xtls.github.io/config/geodata.html).

- pass the top-level "geodata" key through xray.Config so it survives
  the template round-trip into the generated config
- add a Geodata Auto-Update section to the Xray Updates modal that
  edits geodata (cron schedule, download outbound, asset list) in the
  config template and restarts Xray on save
- previously downloaded geo files in the bin folder keep working in
  ext: routing rules; the orphaned custom_geo_resources table is left
  in place so existing source URLs stay recoverable
2026-06-10 18:27:12 +02:00
Rouzbeh† 4002be4ade feat: support latest Wireguard features from Xray-core (PRs #5643, #5833, #5850) (#5131)
* feat: support latest Wireguard features from Xray-core

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

* fix

---------

Co-authored-by: Rqzbeh <Rqzbeh@example.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
2026-06-10 17:02:41 +02:00
吉姆·塞尔夫 dbb269cf6a fix(ui): correct inline style syntax between clients count and active clients count on inbounds page (#5114)
* fix(ui): correct inline style syntax in client counts column on inbounds page

* fix(ui): correct inline style syntax between clients count and active clients count on inbounds page
2026-06-10 15:35:21 +02:00
MHSanaei 6c1594693d feat(mtproto): add domain-fronting and essential mtg options
Expose mtg's [domain-fronting] section (ip/port/proxy-protocol) plus
proxy-protocol-listener, prefer-ip, and debug on MTProto inbounds. Each
key is written to the generated mtg-<id>.toml only when set, so mtg's own
defaults apply otherwise. The instance fingerprint now covers these
fields, so editing an option restarts the sidecar.

Since MTProto is mtg-served (not Xray), sniffing does not apply: hide the
Sniffing tab and the Advanced sniffing sub-editor, drop it from the
Advanced "All" JSON view, and emit empty sniffing in the wire payload,
all gated by a new canEnableSniffing predicate.
2026-06-09 12:44:04 +02:00
吉姆·塞尔夫 7d908834a8 fix(ui): correct inline style syntax in client counts column on inbounds page (#5097) 2026-06-08 23:54:18 +02:00
Sanaei 1ca5924a44 feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar (#5076)
* feat(mtproto): add MTProto (FakeTLS) protocol via managed mtg sidecar

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

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

* fix

* fix

* fix: address Copilot review comments on mtproto PR

- web/web.go: create NewMtprotoJob once and reuse for cron + initial run
- mtproto/manager.go: StopAll cleans up per-inbound config files on shutdown
- mtproto/manager.go: CollectTraffic releases mutex before HTTP scrapes to
  avoid blocking Ensure/Reconcile/Remove during network I/O
- database/model/model.go: panic on crypto/rand failure in mtprotoRandomMiddle
  instead of silently producing a weak all-zero secret
- install.sh: fix chmod to handle renamed bin/mtg-linux-arm on armv5/v6/v7
2026-06-08 14:28:19 +02:00
Sanaei af3c808444 fix: default hysteria tls to no utls fingerprint 2026-06-08 13:15:51 +02:00
MHSanaei c6f15cd53f refactor(api)!: move /panel/setting and /panel/xray under /panel/api
Settings and Xray config endpoints now live at /panel/api/setting/* and /panel/api/xray/*, registered under the existing /panel/api group so they inherit the same Bearer-or-session auth (checkAPIAuth) as the rest of the API. An API token is a full-admin credential, so this just makes the surface consistent. The SPA page routes /panel/settings and /panel/xray are unchanged.

BREAKING CHANGE: the old /panel/setting/* and /panel/xray/* paths are removed. External callers must switch to the /panel/api/ prefix. Frontend call sites, API docs, the dev proxy, and the route-documentation test are updated to match.
2026-06-06 16:22:41 +02:00
Sanaei e6c1ce9aa9 feat(nodes): multi-hop node attribution for chained sub-nodes (#4983) (#5005)
* feat(nodes): add stable panel GUID identity (multi-hop phase 0)

Per-panel autoincrement node ids are meaningless one hop away, so in a chained topology (Node1 -> Node2 -> Node3) the master cannot attribute online clients or inbounds to the physical node that hosts them (#4983).

Introduce a stable self-identifier: each panel generates and persists a panelGuid (settings table, mirroring GetSecret), returns it in panel/api/server/status, and the master learns it per node via the heartbeat into a new Node.Guid column. Guarded so an old-build node or a failed probe never clears a known GUID. No behavior change yet - this is the identity foundation Phases 1-2 key on.

Refs #4983

* feat(nodes): attribute inbounds to their origin node by GUID (multi-hop phase 1)

Add Inbound.OriginNodeGuid: the GUID of the panel that physically hosts an inbound. Empty means this panel's own xray; set means it was synced from a node. SetRemoteTraffic now fills it per synced inbound - keeping a non-empty value the node forwarded from its own sub-node (so a transitive inbound stays attributed to the deepest node across hops), and otherwise attributing the node's own local inbounds to that node's GUID. Empty (old-build node without a GUID) leaves the existing node_id-based attribution untouched.

The field rides the existing inbound JSON, so /list propagates it up the chain with no serve-side change. Phase 2 will key per-node online off this instead of the panel-local node_id.

Refs #4983

* feat(nodes): key online status by node GUID end-to-end (multi-hop phase 2)

Replace the panel-local node-id keying of per-node online status with the stable panelGuid, so a client several hops down a node chain is attributed to the node that physically hosts it instead of the intermediate node it syncs through (#4983).

xray/process.go stores each direct node's reported GUID-keyed subtree and merges them (correct at any depth); the service assembles GetOnlineClientsByGuid (own clients under this panel's GUID + every node under its GUID). FetchTrafficSnapshot fetches the new /clients/onlinesByGuid, falling back to the flat /onlines for old-build nodes (keyed under the node's GUID or a master-local synthetic id). The node rollup, the WS onlineByGuid/activeInbounds fields, and the inbounds-page rollup all scope by GUID; local inbounds get their OriginNodeGuid filled with the panel's GUID at serve time so the frontend keys uniformly.

Old-build nodes degrade to the prior flat behaviour via the synthetic node:<id> key. Refs #4983

Refs #4983

* feat(nodes): surface transitive sub-nodes on the master (multi-hop phase 3a)

Each panel publishes read-only summaries of the nodes it manages via GET /panel/api/server/descendants (node API token). The heartbeat job caches each direct node's summaries; GetNodeTree merges them as transitive model.Node projections (Id 0, Transitive=true, ParentGuid = their parent node's GUID) and recomputes InboundCount/OnlineCount/DepletedCount per origin GUID so a direct node shows only its own inbounds and each sub-node shows its own (#4983).

The Nodes-page list endpoint and the heartbeat broadcast now return the tree; GetAll stays direct-only for probing/syncing. One transitive level is surfaced (covers Node1->Node2->Node3); deeper recursion is a follow-up. Backend only - the Nodes-page nested UI lands next.

Refs #4983

* feat(nodes): render transitive sub-nodes nested + read-only on the Nodes page (multi-hop phase 3b)

The Nodes page now shows a node's downstream sub-nodes (learned via the descendants tree) as indented, read-only rows ordered right under their parent: no enable toggle, probe, edit, delete, update, selection, or history expander - just a 'Sub-node' tag whose tooltip names the parent it is reached through. Desktop table and mobile cards both handle it. Transitive rows are keyed by GUID (their Id is 0) so they don't collide with real nodes (#4983).

Rows nest by parentGuid rather than AntD tree-children to avoid clashing with the existing per-row history expander. New labels added to en-US (other locales fall back until translated). Refs #4983

Refs #4983

* i18n(nodes): translate subNode/subNodeTip across all locales

Phase 3b added these two Nodes-page keys (read-only sub-node tag + tooltip) only to en-US; fill in the other 12 locales so the multi-hop sub-node UI is fully localized. The {parent} placeholder is preserved in every translation.

Refs #4983
2026-06-06 12:33:39 +02:00
nima1024m 6ed6f57b5c fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target (#4988)
* fix(panel): normalize XHTTP/sockopt/Reality wire output and validate REALITY target

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

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

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

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

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

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: MHSanaei <ho3ein.sanaei@gmail.com>
2026-06-06 02:40:32 +02:00
MHSanaei 75bc6e8076 fix(inbound-form): wrap long labels and shorten RU pinned-cert label
Long TLS-tab labels overflowed their field in locales with wider strings (e.g. Russian 'Pinned Peer Cert SHA-256'). Add AntD labelWrap to the inbound and outbound form modals so any over-long label wraps onto a second line instead of overflowing, and shorten the Russian pinnedPeerCertSha256 label to fit.

Closes #4986
2026-06-06 01:53:46 +02:00
MHSanaei a8d5d0dfab fix(external-proxy): relabel "Host" as "Address", add per-entry ECH (#4935)
The external proxy "Host" field was bound to dest (the connection address that becomes the link host) but labeled "Host", misleading users into thinking it set a transport host header. Relabel it to "Address" to match what it actually controls.

Add per-entry ECH (echConfigList) to the external proxy schema, form (shown under Force TLS = TLS), the TS link generator, and the Go sub services: ech is emitted on share links and vmess objects, and written into the stream so the JSON subscription picks it up via the existing tlsData reader.
2026-06-05 10:40:11 +02:00
MHSanaei 1a64d7e9de feat(tls): add ocspStapling to certificate config
Expose the OCSP Stapling refresh interval (seconds) on the TLS
certificate object in the inbound security form, defaulting to 3600s
to match xray-core. Covers both file-backed and inline cert shapes.
2026-06-03 17:49:36 +02:00
MHSanaei 55d6729955 fix(nodes): Set Cert from Panel uses the node's own web cert for node inbounds
For an inbound deployed to a node, the button read the central panel's webCertFile/webKeyFile and inserted paths that don't exist on the node, crashing the node's Xray on startup.

Add a token-accessible GET /panel/api/server/getWebCertFiles that returns a panel's own web cert/key paths, Remote.GetWebCertFiles to fetch it from a node, and GET /panel/api/nodes/webCert/:id to proxy it. setCertFromPanel now calls the node endpoint for a node-assigned inbound and the local settings otherwise, warning instead of inserting wrong paths on error/empty.

Fixes #4854
2026-06-03 16:41:02 +02:00
MHSanaei ef8882a5c0 fix(online): scope per-inbound online to inbounds that carried traffic
Multi-inbound clients showed online on every inbound they were attached to. Xray's user-level traffic stat aggregates across all inbounds a client belongs to, so the email signal alone can't say which inbound was used.

Pair it with the inbound-level traffic signal under the same 20s grace and gate the per-inbound rollup on it: a client only shows online on inbounds that actually moved bytes this window. Remote nodes report no per-inbound activity and stay ungated (no regression). Adds GetActiveInboundsByNode, the activeInbounds WS field and POST /panel/api/clients/activeInbounds.

Fixes #4859
2026-06-03 16:19:00 +02:00
MHSanaei e7c11c913a feat(inbounds): per-proxy Pinned Peer Cert SHA-256 + labeled External Proxy form
Redesign the Add Inbound -> Stream External Proxy section into labeled per-entry cards (Force TLS / Host / Port / Remark and, under TLS, SNI / Fingerprint / ALPN) and add a Pinned Peer Cert SHA-256 field with a generate-random-hash button to each entry.

The pin flows end to end into share links: pcs for vmess/vless/trojan/ss (stripped when a proxy forces security off) and the hex-normalized pinSHA256 for Hysteria. JSON and Clash subscriptions emit the native pinnedPeerCertSha256 / pin-sha256 via the cloned stream. Adds the forceTls label across all 13 locales plus frontend and Go tests.
2026-06-03 13:46:54 +02:00
MHSanaei 6ee462ac8e fix(links): use configured domain for panel copy/QR links on loopback
The panel's copy/QR share links are built client-side and fell back to window.location.hostname, so reaching the panel over an SSH tunnel (127.0.0.1/localhost) leaked localhost into the links - unlike the backend subscription path, which falls back to the configured Sub/Web Domain (issue #4829).

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

Closes #4829
2026-06-02 22:52:44 +02:00