mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-20 11:36:06 +00:00
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:
@@ -130,5 +130,6 @@ exports[`inbound transport forms > XhttpForm field structure is stable 1`] = `
|
||||
"Session Placement",
|
||||
"Sequence Placement",
|
||||
"No SSE Header",
|
||||
"XMUX",
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -65,6 +65,69 @@ describe('normalizeXhttpForWire stream-one', () => {
|
||||
expect(out.xmux).toEqual({ maxConcurrency: '16-32' });
|
||||
expect(out).not.toHaveProperty('scMaxEachPostBytes');
|
||||
});
|
||||
|
||||
it('keeps inbound xmux when enableXmux is on (for the share-link extra)', () => {
|
||||
const out = normalizeXhttpForWire({
|
||||
path: '/app',
|
||||
mode: 'auto',
|
||||
enableXmux: true,
|
||||
xmux: { maxConcurrency: '16-32' },
|
||||
}, 'inbound');
|
||||
|
||||
expect(out).not.toHaveProperty('enableXmux');
|
||||
expect(out.xmux).toEqual({ maxConcurrency: '16-32' });
|
||||
});
|
||||
|
||||
it('drops inbound xmux when enableXmux is off', () => {
|
||||
const out = normalizeXhttpForWire({
|
||||
path: '/app',
|
||||
mode: 'auto',
|
||||
enableXmux: false,
|
||||
xmux: { maxConcurrency: '16-32' },
|
||||
}, 'inbound');
|
||||
|
||||
expect(out).not.toHaveProperty('enableXmux');
|
||||
expect(out).not.toHaveProperty('xmux');
|
||||
});
|
||||
|
||||
// xray-core rejects a config with both maxConnections and maxConcurrency.
|
||||
it('drops maxConcurrency when maxConnections is set (xray-core exclusivity)', () => {
|
||||
const out = normalizeXhttpForWire({
|
||||
path: '/app',
|
||||
mode: 'auto',
|
||||
enableXmux: true,
|
||||
xmux: { maxConcurrency: '16-32', maxConnections: 4, hKeepAlivePeriod: 30 },
|
||||
}, 'inbound');
|
||||
|
||||
const xmux = out.xmux as Record<string, unknown>;
|
||||
expect(xmux).not.toHaveProperty('maxConcurrency');
|
||||
expect(xmux.maxConnections).toBe(4);
|
||||
expect(xmux.hKeepAlivePeriod).toBe(30);
|
||||
});
|
||||
|
||||
it('keeps maxConcurrency when maxConnections is 0/unset', () => {
|
||||
const out = normalizeXhttpForWire({
|
||||
path: '/app',
|
||||
mode: 'stream-one',
|
||||
xmux: { maxConcurrency: '16-32', maxConnections: 0 },
|
||||
}, 'outbound');
|
||||
|
||||
const xmux = out.xmux as Record<string, unknown>;
|
||||
expect(xmux.maxConcurrency).toBe('16-32');
|
||||
expect(xmux.maxConnections).toBe(0);
|
||||
});
|
||||
|
||||
it('applies xmux exclusivity on the outbound side too', () => {
|
||||
const out = normalizeXhttpForWire({
|
||||
path: '/app',
|
||||
mode: 'stream-one',
|
||||
xmux: { maxConcurrency: '16-32', maxConnections: '8' },
|
||||
}, 'outbound');
|
||||
|
||||
const xmux = out.xmux as Record<string, unknown>;
|
||||
expect(xmux).not.toHaveProperty('maxConcurrency');
|
||||
expect(xmux.maxConnections).toBe('8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeSockoptForWire', () => {
|
||||
|
||||
Reference in New Issue
Block a user