瀏覽代碼

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 <[email protected]>
Co-authored-by: MHSanaei <[email protected]>
w3struk 20 小時之前
父節點
當前提交
d01d9867e4

+ 6 - 1
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -150,7 +150,12 @@ export function normalizeXhttpForWire(
 
   if (side === 'inbound') {
     if (!enableXmux) delete out.xmux;
-    delete out.scMinPostsIntervalMs;
+    // scMinPostsIntervalMs is a client-only tuning knob that subscriptions
+    // must propagate to clients. Only strip the xray-core default ("30")
+    // or empty values — the literal "30" is a known DPI fingerprint (#5141).
+    if (out.scMinPostsIntervalMs === '' || out.scMinPostsIntervalMs === '30') {
+      delete out.scMinPostsIntervalMs;
+    }
     delete out.uplinkChunkSize;
   }
 

+ 24 - 10
frontend/src/pages/inbounds/form/transport/xhttp.tsx

@@ -40,8 +40,14 @@ export default function XhttpForm({ form }: { form: FormInstance<InboundFormValu
           }))}
         />
       </Form.Item>
-      {xhttpMode === 'packet-up' && (
+      {(xhttpMode === 'packet-up' || xhttpMode === 'auto') && (
         <>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
+            label={t('pages.inbounds.form.maxUploadSize')}
+          >
+            <Input />
+          </Form.Item>
           <Form.Item
             name={['streamSettings', 'xhttpSettings', 'scMaxBufferedPosts']}
             label={t('pages.inbounds.form.maxBufferedUpload')}
@@ -49,20 +55,28 @@ export default function XhttpForm({ form }: { form: FormInstance<InboundFormValu
             <InputNumber />
           </Form.Item>
           <Form.Item
-            name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
-            label={t('pages.inbounds.form.maxUploadSize')}
+            name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
+            label={t('pages.xray.outboundForm.minUploadInterval')}
           >
-            <Input />
+            <Input placeholder="e.g. 50-150" />
           </Form.Item>
         </>
       )}
       {xhttpMode === 'stream-up' && (
-        <Form.Item
-          name={['streamSettings', 'xhttpSettings', 'scStreamUpServerSecs']}
-          label={t('pages.inbounds.form.streamUpServer')}
-        >
-          <Input />
-        </Form.Item>
+        <>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'scMaxBufferedPosts']}
+            label={t('pages.inbounds.form.maxBufferedUpload')}
+          >
+            <InputNumber />
+          </Form.Item>
+          <Form.Item
+            name={['streamSettings', 'xhttpSettings', 'scStreamUpServerSecs']}
+            label={t('pages.inbounds.form.streamUpServer')}
+          >
+            <Input />
+          </Form.Item>
+        </>
       )}
       <Form.Item
         name={['streamSettings', 'xhttpSettings', 'serverMaxHeaderBytes']}

+ 2 - 2
frontend/src/pages/xray/outbounds/transport/xhttp.tsx

@@ -212,14 +212,14 @@ export default function XhttpForm({ form, onXmuxToggle }: XhttpFormProps) {
           const mode = form.getFieldValue([
             'streamSettings', 'xhttpSettings', 'mode',
           ]);
-          if (mode !== 'packet-up') return null;
+          if (mode !== 'packet-up' && mode !== 'auto') return null;
           return (
             <>
               <Form.Item
                 label={t('pages.xray.outboundForm.minUploadInterval')}
                 name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
               >
-                <Input placeholder="30" />
+                <Input placeholder="e.g. 50-150" />
               </Form.Item>
               <Form.Item
                 label={t('pages.xray.outboundForm.maxUploadSizeBytes')}

+ 6 - 3
frontend/src/schemas/protocols/stream/xhttp.ts

@@ -51,9 +51,12 @@ export const XHttpStreamSettingsSchema = z.object({
   serverMaxHeaderBytes: z.number().int().min(0).default(0),
   uplinkHTTPMethod: z.string().default(''),
   headers: WsHeaderMapSchema.default({}),
-  // Outbound-only fields. Server (inbound) listener ignores these. The
-  // panel embeds them in share-link `extra` blobs so the same xhttp
-  // config can roundtrip on both sides.
+  // Client-side fields stored on inbound for subscription propagation.
+  // The server listener ignores them at runtime, but the panel embeds
+  // them in share-link `extra` blobs so the same xhttp config can
+  // round-trip on both sides.
+  // - scMinPostsIntervalMs: preserved when non-default (stripped at '' or '30')
+  // - uplinkChunkSize & noGRPCHeader: outbound-only; stripped from inbound wire
   scMinPostsIntervalMs: z.string().default(''),
   uplinkChunkSize: z.number().int().min(0).default(0),
   noGRPCHeader: z.boolean().default(false),

+ 118 - 0
frontend/src/test/stream-wire-normalize.test.ts

@@ -53,6 +53,28 @@ describe('normalizeXhttpForWire stream-one', () => {
     expect(out).not.toHaveProperty('headers');
   });
 
+  it('preserves non-default scMinPostsIntervalMs on inbound for subscriptions', () => {
+    const out = normalizeXhttpForWire({
+      path: '/app',
+      mode: 'packet-up',
+      scMinPostsIntervalMs: '50-150',
+      enableXmux: false,
+    }, 'inbound');
+
+    expect(out.scMinPostsIntervalMs).toBe('50-150');
+  });
+
+  it('strips empty scMinPostsIntervalMs on inbound', () => {
+    const out = normalizeXhttpForWire({
+      path: '/app',
+      mode: 'packet-up',
+      scMinPostsIntervalMs: '',
+      enableXmux: false,
+    }, 'inbound');
+
+    expect(out).not.toHaveProperty('scMinPostsIntervalMs');
+  });
+
   it('keeps xmux on outbound stream-one', () => {
     const out = normalizeXhttpForWire({
       path: '/app',
@@ -340,6 +362,102 @@ describe('inbound formValuesToWirePayload integration', () => {
     const settings = tls.settings as Record<string, unknown>;
     expect(settings).not.toHaveProperty('fingerprint');
   });
+
+  it('preserves non-default scMinPostsIntervalMs in packet-up inbound wire payload for subscriptions', () => {
+    const values = {
+      remark: 't',
+      enable: true,
+      port: 443,
+      listen: '0.0.0.0',
+      tag: 'in-443',
+      expiryTime: 0,
+      sniffing: { enabled: false },
+      up: 0,
+      down: 0,
+      total: 0,
+      trafficReset: 'never',
+      lastTrafficResetTime: 0,
+      nodeId: null,
+      protocol: 'vless',
+      settings: { clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }], decryption: 'none' },
+      streamSettings: {
+        network: 'xhttp',
+        security: 'reality',
+        realitySettings: {
+          target: 'play.google.com:443',
+          privateKey: 'priv',
+          serverNames: ['play.google.com'],
+          shortIds: ['44003d86dc1e'],
+          settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
+        },
+        xhttpSettings: {
+          path: '/app',
+          host: 'play.google.com',
+          mode: 'packet-up',
+          scMinPostsIntervalMs: '50-150',
+        },
+        sockopt: {},
+      },
+    };
+
+    const parsed = InboundFormSchema.safeParse(values);
+    expect(parsed.success).toBe(true);
+    if (!parsed.success) throw parsed.error;
+
+    const payload = formValuesToWirePayload(parsed.data);
+    const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
+    const xhttp = stream.xhttpSettings as Record<string, unknown>;
+
+    expect(xhttp.scMinPostsIntervalMs).toBe('50-150');
+  });
+
+  it('strips default scMinPostsIntervalMs=30 from inbound wire payload', () => {
+    const values = {
+      remark: 't',
+      enable: true,
+      port: 443,
+      listen: '0.0.0.0',
+      tag: 'in-443',
+      expiryTime: 0,
+      sniffing: { enabled: false },
+      up: 0,
+      down: 0,
+      total: 0,
+      trafficReset: 'never',
+      lastTrafficResetTime: 0,
+      nodeId: null,
+      protocol: 'vless',
+      settings: { clients: [{ id: '7eeb09ed-ae97-400d-a1ce-2485fb904407', email: 'n' }], decryption: 'none' },
+      streamSettings: {
+        network: 'xhttp',
+        security: 'reality',
+        realitySettings: {
+          target: 'play.google.com:443',
+          privateKey: 'priv',
+          serverNames: ['play.google.com'],
+          shortIds: ['44003d86dc1e'],
+          settings: { publicKey: 'pub', fingerprint: 'chrome', spiderX: '/' },
+        },
+        xhttpSettings: {
+          path: '/app',
+          host: 'play.google.com',
+          mode: 'packet-up',
+          scMinPostsIntervalMs: '30',
+        },
+        sockopt: {},
+      },
+    };
+
+    const parsed = InboundFormSchema.safeParse(values);
+    expect(parsed.success).toBe(true);
+    if (!parsed.success) throw parsed.error;
+
+    const payload = formValuesToWirePayload(parsed.data);
+    const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
+    const xhttp = stream.xhttpSettings as Record<string, unknown>;
+
+    expect(xhttp).not.toHaveProperty('scMinPostsIntervalMs');
+  });
 });
 
 describe('freedom outbound sockopt wire payload', () => {

+ 23 - 13
internal/sub/json_service.go

@@ -150,6 +150,16 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
 		defaultDest = host
 	}
 
+	// Per-inbound xmux takes precedence over the global subJsonMux.
+	// When xmux is present inside xhttpSettings, XHTTP multiplexing
+	// is handled by xmux — don't also set the legacy outbound.Mux.
+	mux := s.mux
+	if xhttp, ok := stream["xhttpSettings"].(map[string]any); ok {
+		if _, hasXmux := xhttp["xmux"]; hasXmux {
+			mux = ""
+		}
+	}
+
 	externalProxies, ok := stream["externalProxy"].([]any)
 	hasExternalProxy := ok && len(externalProxies) > 0
 	if !hasExternalProxy {
@@ -197,13 +207,13 @@ func (s *SubJsonService) getConfig(subReq *SubService, inbound *model.Inbound, c
 
 		switch inbound.Protocol {
 		case "vmess":
-			newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, hostMux))
+			newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, jsonMux(mux, hostMux)))
 		case "vless":
-			newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client, hostMux))
+			newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client, jsonMux(mux, hostMux)))
 		case "trojan", "shadowsocks":
-			newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client, hostMux))
+			newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client, jsonMux(mux, hostMux)))
 		case "hysteria":
-			newOutbounds = append(newOutbounds, s.genHy(inbound, newStream, client))
+			newOutbounds = append(newOutbounds, s.genHy(inbound, newStream, client, jsonMux(mux, hostMux)))
 		}
 
 		newOutbounds = append(newOutbounds, s.defaultOutbounds...)
@@ -340,12 +350,12 @@ func jsonMux(global, override string) string {
 	return global
 }
 
-func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, muxOverride string) json_util.RawMessage {
+func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
 	outbound := Outbound{}
 
 	outbound.Protocol = string(inbound.Protocol)
 	outbound.Tag = "proxy"
-	if mux := jsonMux(s.mux, muxOverride); mux != "" {
+	if mux != "" {
 		outbound.Mux = json_util.RawMessage(mux)
 	}
 	outbound.StreamSettings = streamSettings
@@ -366,11 +376,11 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
 	return result
 }
 
-func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, muxOverride string) json_util.RawMessage {
+func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
 	outbound := Outbound{}
 	outbound.Protocol = string(inbound.Protocol)
 	outbound.Tag = "proxy"
-	if mux := jsonMux(s.mux, muxOverride); mux != "" {
+	if mux != "" {
 		outbound.Mux = json_util.RawMessage(mux)
 	}
 	outbound.StreamSettings = streamSettings
@@ -395,7 +405,7 @@ func (s *SubJsonService) genVless(inbound *model.Inbound, streamSettings json_ut
 	return result
 }
 
-func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, muxOverride string) json_util.RawMessage {
+func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, mux string) json_util.RawMessage {
 	outbound := Outbound{}
 
 	serverData := make([]ServerSetting, 1)
@@ -422,7 +432,7 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
 
 	outbound.Protocol = string(inbound.Protocol)
 	outbound.Tag = "proxy"
-	if mux := jsonMux(s.mux, muxOverride); mux != "" {
+	if mux != "" {
 		outbound.Mux = json_util.RawMessage(mux)
 	}
 	outbound.StreamSettings = streamSettings
@@ -448,14 +458,14 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
 	return result
 }
 
-func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client) json_util.RawMessage {
+func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client, mux string) json_util.RawMessage {
 	outbound := Outbound{}
 
 	outbound.Protocol = string(inbound.Protocol)
 	outbound.Tag = "proxy"
 
-	if s.mux != "" {
-		outbound.Mux = json_util.RawMessage(s.mux)
+	if mux != "" {
+		outbound.Mux = json_util.RawMessage(mux)
 	}
 
 	var settings, stream map[string]any

+ 81 - 0
internal/sub/json_service_test.go

@@ -173,3 +173,84 @@ func TestSubJsonServiceServerUsesServersArray(t *testing.T) {
 		t.Fatalf("shadowsocks server entry must carry method: %#v", ssServer)
 	}
 }
+
+func TestSubJsonServiceXmuxSuppressesGlobalMux(t *testing.T) {
+	globalMux := `{"enabled":true,"concurrency":8}`
+	svc := NewSubJsonService(globalMux, "", "", nil)
+
+	// When xmux is present in xhttpSettings, the per-inbound xmux handles
+	// multiplexing and the legacy outbound.Mux must NOT be set.
+	stream := `{"network":"xhttp","security":"tls","tlsSettings":{"serverName":"example.com"},"xhttpSettings":{"path":"/api","mode":"packet-up","xmux":{"maxConcurrency":"16-32"}}}`
+	parsed := svc.streamData(stream)
+
+	mux := globalMux
+	if xhttp, ok := parsed["xhttpSettings"].(map[string]any); ok {
+		if _, hasXmux := xhttp["xmux"]; hasXmux {
+			mux = ""
+		}
+	}
+
+	streamSettings, _ := json.Marshal(parsed)
+	inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
+	client := model.Client{ID: "uuid-1"}
+
+	raw := svc.genVless(inbound, streamSettings, client, mux)
+	var ob map[string]any
+	if err := json.Unmarshal(raw, &ob); err != nil {
+		t.Fatalf("unmarshal outbound: %v", err)
+	}
+	if _, has := ob["mux"]; has {
+		t.Fatal("outbound.Mux must NOT be set when per-inbound xmux is present")
+	}
+
+	// Verify xmux is still inside xhttpSettings in streamSettings.
+	ss, _ := ob["streamSettings"].(map[string]any)
+	if ss == nil {
+		t.Fatal("streamSettings missing from outbound")
+	}
+	xhttp, _ := ss["xhttpSettings"].(map[string]any)
+	if xhttp == nil {
+		t.Fatal("xhttpSettings missing from streamSettings")
+	}
+	xmux, _ := xhttp["xmux"].(map[string]any)
+	if xmux == nil {
+		t.Fatal("xmux missing from xhttpSettings — per-inbound xmux must survive streamData()")
+	}
+	if xmux["maxConcurrency"] != "16-32" {
+		t.Fatalf("xmux.maxConcurrency = %v, want 16-32", xmux["maxConcurrency"])
+	}
+}
+
+func TestSubJsonServiceGlobalMuxWhenNoXmux(t *testing.T) {
+	globalMux := `{"enabled":true,"concurrency":8}`
+	svc := NewSubJsonService(globalMux, "", "", nil)
+
+	// When no xmux is present, the global subJsonMux should be used.
+	stream := `{"network":"xhttp","security":"tls","tlsSettings":{"serverName":"example.com"},"xhttpSettings":{"path":"/api","mode":"packet-up"}}`
+	parsed := svc.streamData(stream)
+
+	mux := globalMux
+	if xhttp, ok := parsed["xhttpSettings"].(map[string]any); ok {
+		if _, hasXmux := xhttp["xmux"]; hasXmux {
+			mux = ""
+		}
+	}
+
+	streamSettings, _ := json.Marshal(parsed)
+	inbound := &model.Inbound{Listen: "1.2.3.4", Port: 443, Protocol: model.VLESS, Settings: `{"encryption":"none"}`}
+	client := model.Client{ID: "uuid-1"}
+
+	raw := svc.genVless(inbound, streamSettings, client, mux)
+	var ob map[string]any
+	if err := json.Unmarshal(raw, &ob); err != nil {
+		t.Fatalf("unmarshal outbound: %v", err)
+	}
+	m, has := ob["mux"]
+	if !has {
+		t.Fatal("outbound.Mux must be set when global subJsonMux is configured and no per-inbound xmux")
+	}
+	mm, _ := m.(map[string]any)
+	if mm["enabled"] != true || mm["concurrency"] != float64(8) {
+		t.Fatalf("mux payload wrong: %#v", m)
+	}
+}

+ 3 - 3
internal/sub/mutation_audit_test.go

@@ -66,9 +66,9 @@ func TestSubJsonService_MuxAttachedWhenConfigured(t *testing.T) {
 		wantMux  bool
 		protocol model.Protocol
 	}{
-		{"vmess mux", NewSubJsonService(mux, "", "", nil).genVnext(&model.Inbound{Protocol: model.VMESS, Settings: `{}`}, nil, client, ""), true, model.VMESS},
-		{"vless mux", NewSubJsonService(mux, "", "", nil).genVless(&model.Inbound{Protocol: model.VLESS, Settings: `{}`}, nil, client, ""), true, model.VLESS},
-		{"server mux", NewSubJsonService(mux, "", "", nil).genServer(&model.Inbound{Protocol: model.Trojan, Settings: `{}`}, nil, client, ""), true, model.Trojan},
+		{"vmess mux", NewSubJsonService(mux, "", "", nil).genVnext(&model.Inbound{Protocol: model.VMESS, Settings: `{}`}, nil, client, mux), true, model.VMESS},
+		{"vless mux", NewSubJsonService(mux, "", "", nil).genVless(&model.Inbound{Protocol: model.VLESS, Settings: `{}`}, nil, client, mux), true, model.VLESS},
+		{"server mux", NewSubJsonService(mux, "", "", nil).genServer(&model.Inbound{Protocol: model.Trojan, Settings: `{}`}, nil, client, mux), true, model.Trojan},
 		{"vmess no mux", NewSubJsonService("", "", "", nil).genVnext(&model.Inbound{Protocol: model.VMESS, Settings: `{}`}, nil, client, ""), false, model.VMESS},
 		{"vless no mux", NewSubJsonService("", "", "", nil).genVless(&model.Inbound{Protocol: model.VLESS, Settings: `{}`}, nil, client, ""), false, model.VLESS},
 		{"server no mux", NewSubJsonService("", "", "", nil).genServer(&model.Inbound{Protocol: model.Trojan, Settings: `{}`}, nil, client, ""), false, model.Trojan},