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>
This commit is contained in:
Rouzbeh†
2026-06-12 12:31:13 +02:00
committed by GitHub
parent 63a6d40457
commit 0766e16684
6 changed files with 190 additions and 4 deletions
@@ -399,11 +399,15 @@ describe('outbound-form-adapter: xhttp xmux toggle', () => {
});
});
it('round-trips xmux on save and strips the UI-only enableXmux flag', () => {
it('round-trips xmux on save, strips enableXmux, and enforces xmux exclusivity', () => {
const back = formValuesToWirePayload(rawOutboundToFormValues(xmuxWire));
const xhttp = (back.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
expect(xhttp).not.toHaveProperty('enableXmux');
expect(xhttp.xmux).toMatchObject({ maxConcurrency: '11', maxConnections: '1' });
const xmux = xhttp.xmux as Record<string, unknown>;
// xray-core rejects maxConnections + maxConcurrency together; the
// explicit maxConnections wins and maxConcurrency is dropped.
expect(xmux).not.toHaveProperty('maxConcurrency');
expect(xmux).toMatchObject({ maxConnections: '1', hMaxRequestTimes: '1', hMaxReusableSecs: '1' });
});
it('drops xmux on save when the toggle is off', () => {