Skip to content

fix(sub): preserve non-default scMinPostsIntervalMs and use per-inbound xmux in JSON subscriptions#5393

Merged
MHSanaei merged 8 commits into
MHSanaei:mainfrom
w3struk:fix/json-sub-xhttp-client-fields
Jun 19, 2026
Merged

fix(sub): preserve non-default scMinPostsIntervalMs and use per-inbound xmux in JSON subscriptions#5393
MHSanaei merged 8 commits into
MHSanaei:mainfrom
w3struk:fix/json-sub-xhttp-client-fields

Conversation

@w3struk

@w3struk w3struk commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Problem

JSON subscriptions generated by 3x-ui had two bugs that prevented XHTTP client tuning knobs from reaching subscribers, and the inbound/outbound forms had mode-conditional visibility issues that hid important fields.

1. scMinPostsIntervalMs never reaches the DB

The frontend wire normalizer (normalizeXhttpForWire) unconditionally deleted scMinPostsIntervalMs from inbound configs before persisting to the database:

if (side === 'inbound') {
    delete out.scMinPostsIntervalMs;  // always deleted, even non-default values
}

This meant that even when an admin set a custom value like "50-150", it was silently dropped on save. JSON subscriptions could never include it because it was never stored.

The xray-core default "30" is a known DPI fingerprint (#5141) and must still be stripped, but non-default tuning values must survive the round-trip so that buildXhttpExtra and the JSON subscription generator can propagate them to clients.

2. JSON subscriptions ignore per-inbound xmux

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 with xmux still got the legacy mux.cool block injected
  • The inbound's own xmux was silently ignored in JSON subscriptions
  • Share links (vless://) already read per-inbound xmux via buildXhttpExtra — JSON subscriptions were the only format that didn't

3. Inbound form hides server-side fields behind mode gates

scMaxEachPostBytes is a server-side field used by xray-core in every mode (both handlePacketUp and handleStreamUp validate it). It was hidden behind the packet-up/auto conditional, making it invisible in stream-up and stream-one modes.

scMaxBufferedPosts is only used by handlePacketUp and correctly belongs behind the packet-up/auto gate.

scMinPostsIntervalMs is client-only and packet-up/auto-specific — it was missing from the inbound form entirely.

4. Outbound form hides scMinPostsIntervalMs for mode: "auto"

Because xray-core resolves client mode: "auto" with TLS to packet-up, the scMinPostsIntervalMs field must be visible for auto mode too — otherwise users cannot configure it for the most common TLS+XHTTP setup.

Commits

Commit 1: Preserve non-default scMinPostsIntervalMs in inbound wire payload

File: frontend/src/lib/xray/stream-wire-normalize.ts

Instead of unconditionally deleting scMinPostsIntervalMs, only strip the xray-core default ("30") and empty values. Custom tuning values like "50-150" now survive the round-trip.

Commit 2: Use per-inbound xmux instead of global subJsonMux in JSON subscriptions

File: internal/sub/json_service.go

getConfig() now 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 (backward compatible).

Commit 3: Add scMinPostsIntervalMs to inbound XHTTP form

File: frontend/src/pages/inbounds/form/transport/xhttp.tsx

Adds the missing UI field for scMinPostsIntervalMs in the inbound form's packet-up section, so admins can set anti-DPI intervals directly from the panel.

Commit 4: Show packet-up fields for auto mode in inbound XHTTP form

File: frontend/src/pages/inbounds/form/transport/xhttp.tsx

Because xray-core resolves client mode: "auto" with TLS to packet-up, the packet-up fields (including scMinPostsIntervalMs) are now also shown when the inbound mode is auto.

Commit 5: Show scMinPostsIntervalMs for auto mode in outbound form, update placeholder

File: frontend/src/pages/xray/outbounds/transport/xhttp.tsx

Same consistency fix for the outbound form: scMinPostsIntervalMs is now visible for mode: "auto". Placeholder changed from "30" (DPI fingerprint) to "e.g. 50-150".

Commit 6: Show scMaxEachPostBytes for all modes in inbound form

File: frontend/src/pages/inbounds/form/transport/xhttp.tsx

scMaxEachPostBytes is used by xray-core in every mode (both handlePacketUp and handleStreamUp validate it). It is now always visible regardless of mode. scMaxBufferedPosts remains gated behind packet-up/auto since it is only used by handlePacketUp.

Commit 7: Update XhttpForm snapshot

File: frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap

Snapshot updated to reflect scMaxEachPostBytes being unconditionally visible.

Why these changes matter

  • Anti-DPI: The default scMinPostsIntervalMs: 30 is a known fingerprint. Allowing users to set ranges like "50-150" and propagating them to subscriptions reduces fingerprinting.
  • XHTTP xmux correctness: mux.cool and XHTTP xmux are different multiplexers. Using the global subJsonMux (mux.cool) for XHTTP outbounds caused many short-lived TCP connections, which is also fingerprintable. Per-inbound xmux fixes this.
  • Mode consistency: We now default to explicit stream-up in our deployment template because auto with TLS resolves to packet-up in xray-core, contrary to what the public docs claim.
  • Form completeness: Server-side fields used in all modes should not be hidden behind client-mode gates. Client-only fields should only appear for relevant modes.

Backward Compatibility

Scenario Before After
Inbound without xmux, global subJsonMux set outbound.Mux from global Same
Inbound with xmux, global subJsonMux set outbound.Mux from global (double-mux) outbound.Mux suppressed, xmux in xhttpSettings
scMinPostsIntervalMs = "30" (default) Deleted Deleted
scMinPostsIntervalMs = "50-150" (custom) Deleted (bug) Preserved
scMinPostsIntervalMs = "" (empty) Deleted Deleted

Testing

Deployed to a test server and verified:

  • scMinPostsIntervalMs: "50-150" appears in JSON subscription
  • xmux: {"maxConcurrency": "16-32", "hMaxRequestTimes": "600-900"} appears inside xhttpSettings
  • outbound.Mux is suppressed when xmux is present
  • Without xmux, global subJsonMux is used as outbound.Mux
  • scMaxEachPostBytes is visible in all modes in the inbound form
  • scMaxBufferedPosts is visible only for packet-up and auto modes
  • scMinPostsIntervalMs is visible for packet-up and auto modes in both inbound and outbound forms

w3struk added 2 commits June 16, 2026 23:50
…ayload

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 (MHSanaei#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.
…ubscriptions

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
@ilya-practicum

Copy link
Copy Markdown

btw, currently ui missing scMinPostsIntervalMs parameter at all

w3struk added 4 commits June 17, 2026 09:17
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 MHSanaei#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
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.
…older

- 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
…edPosts 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.
@w3struk w3struk force-pushed the fix/json-sub-xhttp-client-fields branch from 04c31d1 to a218d8b Compare June 17, 2026 07:08
…ification

- 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.
@ilya-practicum

Copy link
Copy Markdown

Thx

# Conflicts:
#	internal/sub/json_service.go
#	internal/sub/json_service_test.go
#	internal/sub/mutation_audit_test.go
@MHSanaei MHSanaei merged commit d01d986 into MHSanaei:main Jun 19, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants