Преглед изворни кода

refactor(frontend): retire class-based xray models (Step 5)

Delete models/inbound.ts (3,359 lines) and outbound.ts (2,405).
The Inbound/Outbound classes and ~50 sub-classes are replaced by
Zod-typed data + pure functions in lib/xray/*.

Consumer migration off dbInbound.toInbound():
- useInbounds: isSSMultiUser({protocol, settings}) directly
- QrCodeModal: genWireguardConfigs/Links/AllLinks from lib/xray
- InboundList: derives tags from streamSettings raw fields
- InboundsPage: clone via raw JSON, fallback projection via
  schema-shape stream object, exports via genInboundLinks
- InboundInfoModal: builds an InboundInfo facade locally from
  raw streamSettings (host/path/serverName/serviceName per
  network), canEnableTlsFlow + isSS2022 from lib/xray

New helper: lib/xray/inbound-from-db.ts exposes
inboundFromDb(raw) converting a raw DBInbound row into a
schema-typed Inbound for the link-generation orchestrators.

DBInbound trimmed: drops toInbound, isMultiUser, hasLink,
genInboundLinks, _cachedInbound. Imports Protocols from
@/schemas/primitives now that ./inbound is gone.

Bundled Phase 2 fixes:
- Outbound modal: Form.useWatch with preserve: true so the
  stream block doesn't gate itself out when network is unmounted
- Inbound form adapter: pruneEmpty preserves empty objects;
  per-protocol client field projection via Zod safeParse;
  sniffing collapse to {enabled:false}
- useClients invalidateAll also invalidates inbounds.root()
- IndexPage Config modal top/maxHeight polish

Tests: 283/283 pass. typecheck/lint clean.
MHSanaei пре 1 дан
родитељ
комит
f92f07e8f2

+ 0 - 132
frontend/ZOD_MIGRATION_STATUS.md

@@ -1,132 +0,0 @@
-# 3x-ui Frontend Zod Migration — Status
-
-Branch: `feat/frontend-zod-validation` · 83 commits ahead of `main`
-
-Last updated: 2026-05-26
-
-## What this is
-
-The work tracked here is the migration described in
-`C:\Users\Hossein Sanaei\.claude\plans\zod-soft-feather.md` — replacing the
-class-based xray models (`models/inbound.ts`, `models/outbound.ts`) with Zod
-schemas as the single source of truth, standardizing every form on AntD
-`Form.useForm` + `antdRule(schema.shape.X)`, and tightening
-`@typescript-eslint/no-explicit-any` to `error`.
-
-Verify state: `npm run typecheck` clean, `npm run lint` clean,
-`npm run test` 302/302, snapshot baselines 172/172.
-
----
-
-## Done
-
-### Foundations
-
-- API-boundary Zod validation in TanStack Query hooks (`parseMsg` helper)
-- Backend request-body validation via `go-playground/validator`
-- Go-first codegen tool (`tools/openapigen`) emitting `zod.ts` + `types.ts`
-- `antdRule(schema)` helper bridging Zod issues to AntD form rules
-- Five secondary modals migrated to Pattern A (Login, 2FA, Geo, Balancer, Rule)
-- Pre-save schema guard on Inbound/Outbound form submits
-
-### Schemas — `frontend/src/schemas/`
-
-- `primitives/` — port, protocol, sniffing, atomic dictionaries
-- `protocols/inbound/*` — 10 protocols as leaf schemas
-- `protocols/outbound/*` — 11 protocols as leaf schemas
-- `protocols/stream/*` — 7 networks (tcp/kcp/ws/grpc/httpupgrade/xhttp/hysteria)
-- `protocols/security/*` — 3 securities (none/tls/reality)
-- `forms/inbound-form.ts` — `InboundFormValues` discriminated union
-- `forms/outbound-form.ts` — `OutboundFormValues` discriminated union
-- Stream + security families wired as `z.discriminatedUnion` with intersection
-
-### Pure-function ports — `frontend/src/lib/xray/`
-
-- `headers.ts` — `toHeaders`, `toV2Headers`, `getHeaderValue`
-- `inbound-link.ts` — `genVmessLink`, `genVlessLink`, `genTrojanLink`,
-  `genShadowsocksLink`, `genHysteriaLink`, Wireguard link/config
-- `outbound-link-parser.ts` — vmess/vless/trojan/shadowsocks/hysteria2
-- `inbound-defaults.ts` — `createDefault{Vmess,Vless,...}{Client,InboundSettings}`
-- `outbound-defaults.ts` — settings factories + dispatcher
-- `outbound-form-adapter.ts` — raw ↔ `OutboundFormValues` round-trip
-- `protocol-capabilities.ts` — capability predicates as pure functions
-
-### Form modals on Pattern A
-
-- `InboundFormModal.tsx` — full rewrite, atomic-swapped from `.new.tsx`
-  - Tabs: Basic, Sniffing, Protocol, Stream, Security, Advanced JSON,
-    Fallbacks
-  - All 10 protocols (VLESS, VMess, Trojan, Shadowsocks, HTTP, Mixed,
-    Tunnel, TUN, Wireguard, Hysteria)
-  - Full Stream tab (TCP, KCP, WS, gRPC, HTTPUpgrade, XHTTP, Hysteria)
-  - Full Security tab (TLS list, Reality, ECH, mldsa65)
-  - 18-field sockopt section, full TLS cert list, external-proxy section
-- `OutboundFormModal.tsx` — full rewrite, atomic-swapped from `.new.tsx`
-  - All 12 protocols (vmess/vless/trojan/shadowsocks/socks/http/hysteria/
-    freedom/blackhole/dns/loopback/wireguard)
-  - Full Stream tab with XHTTP advanced fields + xmux sub-form
-  - Full Security tab (TLS + Reality + Vision flow)
-  - Sockopt section (17 knobs)
-  - Mux section
-  - JSON tab for advanced fields
-  - Link import (vmess/vless/trojan/ss/hysteria2) with full XHTTP
-    round-trip (padding obfs + session/seq/uplink keys + all post-size
-    knobs)
-- `FinalMaskForm` rewritten to Pattern A (Form.List-driven) and wired
-  into both stream tabs (Inbound + Outbound). Covers TCP/UDP mask
-  arrays, all 13 UDP mask types, header-custom nested groups, noise
-  items, and the QUIC params sub-form.
-
-### Tests
-
-- Golden-file fixture suite (`test/golden/fixtures/`)
-- Snapshot-baseline regression tests for inbound-full / outbound / stream /
-  security DUs
-- Shadow-parse harness asserting legacy class and Zod converge
-- Link-parser tests (15 round-trip cases including XHTTP padding-obfs)
-- Outbound form-adapter tests (15 round-trip cases)
-- 302 tests across 12 files, 172 snapshots
-
-### Build infrastructure
-
-- `@typescript-eslint/no-explicit-any: 'error'` enforced
-- `.github/workflows/ci.yml` runs `typecheck` + `test` before `build`
-- Vite pinned to 8.0.13 (dev-mode dep-optimizer regression in 8.0.14)
-
----
-
-## Remaining
-
-### Out of migration scope (per plan)
-
-- `DBInbound`, `Status`, `AllSetting` legacy classes — flagged as out of
-  scope in `zod-soft-feather.md`. The mainline migration of
-  `models/inbound.ts` / `models/outbound.ts` cannot delete them entirely
-  while `DBInbound.toInbound()` still imports.
-- The plan accepts this and treats parity via snapshot baselines instead.
-
-### Nice-to-haves — would not block ship
-
-- Reality `sid=` multi-value parsing in share-link import
-  (outbound reality only carries a single shortId — this is server-side
-  state)
-- `fm=` (FinalMask) param in share-link import
-- VMess link `xmux` nested JSON parsing (currently round-trips at the
-  XHTTP top level; nested xmux object is left empty)
-- Tighter `.loose()` removal in `schemas/api/inbound.ts`,
-  `schemas/api/client.ts`, `schemas/xray.ts` — gated on Step 6 of the plan
-  (currently held because the codegen tool still emits one or two loose
-  fields the panel writes back)
-
----
-
-## How to pick up where this left off
-
-1. `git checkout feat/frontend-zod-validation`
-2. `cd frontend && npm install && npm run typecheck && npm run test`
-3. Open `C:\Users\Hossein Sanaei\.claude\plans\zod-soft-feather.md` —
-   Steps 1–5 are done. Step 6 (tighten `.loose()`) and Step 7 (lint/CI
-   tightening) are partially done.
-4. Nothing in this list blocks ship. The mainline migration goal
-   (replace class-based models with Zod schemas + Pattern A forms) is
-   done; remaining work is incremental polish.

+ 10 - 1
frontend/src/hooks/useClients.ts

@@ -161,8 +161,17 @@ export function useClients() {
   const trafficDiff = ((defaults.trafficDiff as number) ?? 0) * 1073741824;
   const pageSize = (defaults.pageSize as number) ?? 0;
 
+  // Client mutations (add/update/remove/attach/detach/resetTraffic/…) all
+  // mutate inbound rows server-side too — adding a client appends to
+  // settings.clients on each attached inbound, the slim list's per-inbound
+  // client count is derived from that. Invalidate both buckets so the
+  // Inbounds page and any open edit modal pick up the new shape without
+  // a manual reload.
   const invalidateAll = useCallback(
-    () => queryClient.invalidateQueries({ queryKey: keys.clients.root() }),
+    () => Promise.all([
+      queryClient.invalidateQueries({ queryKey: keys.clients.root() }),
+      queryClient.invalidateQueries({ queryKey: keys.inbounds.root() }),
+    ]),
     [queryClient],
   );
 

+ 139 - 3
frontend/src/lib/xray/inbound-form-adapter.ts

@@ -1,7 +1,15 @@
 import type { InboundFormValues, TrafficReset } from '@/schemas/forms/inbound-form';
 import type { InboundSettings } from '@/schemas/protocols/inbound';
+import {
+  HysteriaClientSchema,
+  ShadowsocksClientSchema,
+  TrojanClientSchema,
+  VlessClientSchema,
+  VmessClientSchema,
+} from '@/schemas/protocols/inbound';
 import type { StreamSettings } from '@/schemas/api/inbound';
 import type { Sniffing } from '@/schemas/primitives';
+import type { z } from 'zod';
 
 // Plain-data adapter between the panel's stored inbound row shape and
 // the typed InboundFormValues that Form.useForm<T> carries inside
@@ -79,6 +87,31 @@ function coerceTrafficReset(v: unknown): TrafficReset {
     : 'never';
 }
 
+// Network values that map to a required `${network}Settings` key in
+// NetworkSettingsSchema. Older saved inbounds may be missing the per-
+// network sub-object (the legacy panel sometimes emitted streamSettings
+// without it, and an earlier panel-side prune wrongly stripped empty
+// `tcpSettings: {}` out of the wire payload). Reseat an empty object
+// here so InboundFormSchema.safeParse doesn't blow up at edit time.
+const NETWORK_SETTINGS_KEY: Record<string, string> = {
+  tcp: 'tcpSettings',
+  kcp: 'kcpSettings',
+  ws: 'wsSettings',
+  grpc: 'grpcSettings',
+  httpupgrade: 'httpupgradeSettings',
+  xhttp: 'xhttpSettings',
+  hysteria: 'hysteriaSettings',
+};
+
+function healStreamNetworkKey(stream: Record<string, unknown>): void {
+  const network = typeof stream.network === 'string' ? stream.network : '';
+  const key = NETWORK_SETTINGS_KEY[network];
+  if (!key) return;
+  if (stream[key] == null || typeof stream[key] !== 'object') {
+    stream[key] = {};
+  }
+}
+
 // Map a raw DB row (settings/streamSettings/sniffing as string OR object)
 // into the typed InboundFormValues. Does NOT validate against the schema —
 // callers that want a hard guarantee should follow up with
@@ -90,6 +123,9 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
   const streamSettings = Object.keys(rawStream).length > 0
     ? (rawStream as StreamSettings)
     : undefined;
+  if (streamSettings) {
+    healStreamNetworkKey(streamSettings as unknown as Record<string, unknown>);
+  }
   const sniffing = coerceJsonObject(row.sniffing) as unknown as Sniffing;
 
   return {
@@ -112,7 +148,107 @@ export function rawInboundToFormValues(row: RawInboundRow): InboundFormValues {
   } as InboundFormValues;
 }
 
+// Recursively strip undefined leaves from the wire payload. Empty arrays
+// and empty objects are PRESERVED — legacy XrayCommonClass.toJson() kept
+// shells like `tcpSettings: {}` so xray-core picks up its built-in
+// defaults, and stripping them led the FE to lose required-but-empty
+// arrays (vless clients, wireguard peers, etc.) which the Go side then
+// serialized back as `null`. Primitive values (including 0, false, '')
+// are kept verbatim.
+export function pruneEmpty(value: unknown): unknown {
+  if (Array.isArray(value)) {
+    return value.map(pruneEmpty);
+  }
+  if (value !== null && typeof value === 'object') {
+    const out: Record<string, unknown> = {};
+    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
+      const p = pruneEmpty(v);
+      if (p === undefined) continue;
+      out[k] = p;
+    }
+    return out;
+  }
+  return value;
+}
+
+// Per-protocol client field whitelist — the Zod schemas in
+// schemas/protocols/inbound/<proto>.ts define which keys a given
+// protocol's clients accept on the wire. When a global client is created
+// the panel may persist cross-protocol fields on the same row (`auth` for
+// hysteria, `password` for trojan, `security` for vmess, etc.); rendering
+// those inside a vless inbound's settings.clients is confusing and rides
+// dead weight in the wire payload. Parsing through the protocol's schema
+// gives us the canonical projection.
+function clientSchemaForProtocol(protocol: string): z.ZodTypeAny | null {
+  switch (protocol) {
+    case 'vless':       return VlessClientSchema;
+    case 'vmess':       return VmessClientSchema;
+    case 'trojan':      return TrojanClientSchema;
+    case 'shadowsocks': return ShadowsocksClientSchema;
+    case 'hysteria':    return HysteriaClientSchema;
+    default:            return null;
+  }
+}
+
+export function normalizeClients(protocol: string, clients: unknown): unknown {
+  const schema = clientSchemaForProtocol(protocol);
+  if (!schema || !Array.isArray(clients)) return clients;
+  return clients.map((c) => {
+    const parsed = schema.safeParse(c);
+    return parsed.success ? parsed.data : c;
+  });
+}
+
+// Sniffing normalizer matching the legacy Sniffing.toJson(): when
+// disabled the payload is the bare `{ enabled: false }` regardless of
+// what the form holds; when enabled, only non-default fields ride.
+export function normalizeSniffing(s: Sniffing | undefined): Record<string, unknown> {
+  if (!s || !s.enabled) return { enabled: false };
+  const out: Record<string, unknown> = {
+    enabled: true,
+    destOverride: s.destOverride,
+  };
+  if (s.metadataOnly) out.metadataOnly = true;
+  if (s.routeOnly) out.routeOnly = true;
+  if (s.ipsExcluded?.length) out.ipsExcluded = s.ipsExcluded;
+  if (s.domainsExcluded?.length) out.domainsExcluded = s.domainsExcluded;
+  return out;
+}
+
+// Drops cosmetic empty-array keys that legacy XrayCommonClass.toJson()
+// explicitly skipped (fallbacks/finalmask). Mutates the pruned settings
+// objects in place; called AFTER pruneEmpty so we can lean on the
+// already-shallow shape.
+export function dropLegacyOptionalEmpties(
+  settings: Record<string, unknown>,
+  stream: Record<string, unknown> | undefined,
+): void {
+  // VLESS/Trojan emit `fallbacks` only when non-empty.
+  const fb = settings.fallbacks;
+  if (Array.isArray(fb) && fb.length === 0) delete settings.fallbacks;
+
+  // StreamSettings emits `finalmask` only when at least one transport
+  // mask exists (legacy `hasFinalMask`). Otherwise drop the whole block.
+  if (stream) {
+    const fm = stream.finalmask as { tcp?: unknown[]; udp?: unknown[]; quicParams?: unknown } | undefined;
+    if (fm && typeof fm === 'object') {
+      const hasTcp = Array.isArray(fm.tcp) && fm.tcp.length > 0;
+      const hasUdp = Array.isArray(fm.udp) && fm.udp.length > 0;
+      const hasQuic = fm.quicParams != null;
+      if (!hasTcp && !hasUdp && !hasQuic) delete stream.finalmask;
+    }
+  }
+}
+
 export function formValuesToWirePayload(values: InboundFormValues): WireInboundPayload {
+  const settingsPruned = (pruneEmpty(values.settings ?? {}) ?? {}) as Record<string, unknown>;
+  if (Array.isArray(settingsPruned.clients)) {
+    settingsPruned.clients = normalizeClients(values.protocol, settingsPruned.clients);
+  }
+  const streamPruned = values.streamSettings
+    ? ((pruneEmpty(values.streamSettings) ?? {}) as Record<string, unknown>)
+    : undefined;
+  dropLegacyOptionalEmpties(settingsPruned, streamPruned);
   const payload: WireInboundPayload = {
     up: values.up,
     down: values.down,
@@ -125,9 +261,9 @@ export function formValuesToWirePayload(values: InboundFormValues): WireInboundP
     listen: values.listen,
     port: values.port,
     protocol: values.protocol,
-    settings: JSON.stringify(values.settings ?? {}),
-    streamSettings: values.streamSettings ? JSON.stringify(values.streamSettings) : '',
-    sniffing: JSON.stringify(values.sniffing ?? {}),
+    settings: JSON.stringify(settingsPruned),
+    streamSettings: streamPruned ? JSON.stringify(streamPruned) : '',
+    sniffing: JSON.stringify(normalizeSniffing(values.sniffing)),
     tag: values.tag,
   };
   if (values.nodeId != null) payload.nodeId = values.nodeId;

+ 39 - 0
frontend/src/lib/xray/inbound-from-db.ts

@@ -0,0 +1,39 @@
+import type { Inbound } from '@/schemas/api/inbound';
+import { coerceInboundJsonField } from '@/models/dbinbound';
+
+export interface DbInboundLike {
+  port: number;
+  listen: string;
+  protocol: string;
+  settings: unknown;
+  streamSettings: unknown;
+  sniffing: unknown;
+  tag?: string;
+  remark?: string;
+  enable?: boolean;
+  expiryTime?: number;
+  up?: number;
+  down?: number;
+  total?: number;
+}
+
+export function inboundFromDb(raw: DbInboundLike): Inbound {
+  const settings = coerceInboundJsonField(raw.settings);
+  const streamSettings = coerceInboundJsonField(raw.streamSettings);
+  const sniffing = coerceInboundJsonField(raw.sniffing);
+  return {
+    protocol: raw.protocol,
+    port: raw.port,
+    listen: raw.listen ?? '',
+    tag: raw.tag ?? '',
+    remark: raw.remark ?? '',
+    enable: raw.enable ?? true,
+    expiryTime: raw.expiryTime ?? 0,
+    up: raw.up ?? 0,
+    down: raw.down ?? 0,
+    total: raw.total ?? 0,
+    settings,
+    streamSettings,
+    sniffing,
+  } as unknown as Inbound;
+}

+ 1 - 58
frontend/src/models/dbinbound.ts

@@ -1,6 +1,6 @@
 import dayjs, { type Dayjs } from 'dayjs';
 import { ObjectUtil, NumberFormatter, SizeFormatter } from '@/utils';
-import { Inbound, Protocols } from './inbound';
+import { Protocols } from '@/schemas/primitives';
 
 export type RawJsonField = string | Record<string, unknown> | unknown[];
 
@@ -85,7 +85,6 @@ export class DBInbound {
     nodeId: number | null;
     fallbackParent: FallbackParentRef | null;
 
-    private _cachedInbound: Inbound | null = null;
     private _clientStatsMap: Map<string, ClientStats> | null = null;
 
     constructor(data?: DBInboundInit) {
@@ -184,34 +183,9 @@ export class DBInbound {
     }
 
     invalidateCache(): void {
-        this._cachedInbound = null;
         this._clientStatsMap = null;
     }
 
-    toInbound(): Inbound {
-        if (this._cachedInbound) {
-            return this._cachedInbound;
-        }
-
-        const settings = coerceInboundJsonField(this.settings);
-        const streamSettings = coerceInboundJsonField(this.streamSettings);
-        const sniffing = coerceInboundJsonField(this.sniffing);
-
-        const config = {
-            port: this.port,
-            listen: this.listen,
-            protocol: this.protocol,
-            settings: settings,
-            streamSettings: streamSettings,
-            tag: this.tag,
-            sniffing: sniffing,
-            clientStats: this.clientStats,
-        };
-
-        this._cachedInbound = Inbound.fromJson(config);
-        return this._cachedInbound;
-    }
-
     getClientStats(email: string): ClientStats | undefined {
         if (!this._clientStatsMap) {
             this._clientStatsMap = new Map();
@@ -226,35 +200,4 @@ export class DBInbound {
         return this._clientStatsMap.get(email);
     }
 
-    isMultiUser(): boolean {
-        switch (this.protocol) {
-            case Protocols.VMESS:
-            case Protocols.VLESS:
-            case Protocols.TROJAN:
-            case Protocols.HYSTERIA:
-                return true;
-            case Protocols.SHADOWSOCKS:
-                return this.toInbound().isSSMultiUser;
-            default:
-                return false;
-        }
-    }
-
-    hasLink(): boolean {
-        switch (this.protocol) {
-            case Protocols.VMESS:
-            case Protocols.VLESS:
-            case Protocols.TROJAN:
-            case Protocols.SHADOWSOCKS:
-            case Protocols.HYSTERIA:
-                return true;
-            default:
-                return false;
-        }
-    }
-
-    genInboundLinks(remarkModel: string, hostOverride: string = ''): string {
-        const inbound = this.toInbound();
-        return inbound.genInboundLinks(this.remark, remarkModel, hostOverride);
-    }
 }

+ 0 - 3359
frontend/src/models/inbound.ts

@@ -1,3359 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import dayjs from 'dayjs';
-import { ObjectUtil, RandomUtil, Base64, NumberFormatter, SizeFormatter, Wireguard } from '@/utils';
-import { getRandomRealityTarget } from '@/models/reality-targets';
-
-export const Protocols = {
-    VMESS: 'vmess',
-    VLESS: 'vless',
-    TROJAN: 'trojan',
-    SHADOWSOCKS: 'shadowsocks',
-    WIREGUARD: 'wireguard',
-    HYSTERIA: 'hysteria',
-    MIXED: 'mixed',
-    HTTP: 'http',
-    TUNNEL: 'tunnel',
-    TUN: 'tun',
-};
-
-export const SSMethods = {
-    AES_256_GCM: 'aes-256-gcm',
-    CHACHA20_POLY1305: 'chacha20-poly1305',
-    CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
-    XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
-    BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
-    BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
-    BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
-};
-
-export const TLS_FLOW_CONTROL = {
-    VISION: "xtls-rprx-vision",
-    VISION_UDP443: "xtls-rprx-vision-udp443",
-};
-
-export const TLS_VERSION_OPTION = {
-    TLS10: "1.0",
-    TLS11: "1.1",
-    TLS12: "1.2",
-    TLS13: "1.3",
-};
-
-export const TLS_CIPHER_OPTION = {
-    AES_128_GCM: "TLS_AES_128_GCM_SHA256",
-    AES_256_GCM: "TLS_AES_256_GCM_SHA384",
-    CHACHA20_POLY1305: "TLS_CHACHA20_POLY1305_SHA256",
-    ECDHE_ECDSA_AES_128_CBC: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
-    ECDHE_ECDSA_AES_256_CBC: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
-    ECDHE_RSA_AES_128_CBC: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
-    ECDHE_RSA_AES_256_CBC: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
-    ECDHE_ECDSA_AES_128_GCM: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
-    ECDHE_ECDSA_AES_256_GCM: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
-    ECDHE_RSA_AES_128_GCM: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
-    ECDHE_RSA_AES_256_GCM: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
-    ECDHE_ECDSA_CHACHA20_POLY1305: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
-    ECDHE_RSA_CHACHA20_POLY1305: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
-};
-
-export const UTLS_FINGERPRINT = {
-    UTLS_CHROME: "chrome",
-    UTLS_FIREFOX: "firefox",
-    UTLS_SAFARI: "safari",
-    UTLS_IOS: "ios",
-    UTLS_android: "android",
-    UTLS_EDGE: "edge",
-    UTLS_360: "360",
-    UTLS_QQ: "qq",
-    UTLS_RANDOM: "random",
-    UTLS_RANDOMIZED: "randomized",
-    UTLS_RONDOMIZEDNOALPN: "randomizednoalpn",
-    UTLS_UNSAFE: "unsafe",
-};
-
-export const ALPN_OPTION = {
-    H3: "h3",
-    H2: "h2",
-    HTTP1: "http/1.1",
-};
-
-export const SNIFFING_OPTION = {
-    HTTP: "http",
-    TLS: "tls",
-    QUIC: "quic",
-    FAKEDNS: "fakedns"
-};
-
-export const USAGE_OPTION = {
-    ENCIPHERMENT: "encipherment",
-    VERIFY: "verify",
-    ISSUE: "issue",
-};
-
-export const DOMAIN_STRATEGY_OPTION = {
-    AS_IS: "AsIs",
-    USE_IP: "UseIP",
-    USE_IPV6V4: "UseIPv6v4",
-    USE_IPV6: "UseIPv6",
-    USE_IPV4V6: "UseIPv4v6",
-    USE_IPV4: "UseIPv4",
-    FORCE_IP: "ForceIP",
-    FORCE_IPV6V4: "ForceIPv6v4",
-    FORCE_IPV6: "ForceIPv6",
-    FORCE_IPV4V6: "ForceIPv4v6",
-    FORCE_IPV4: "ForceIPv4",
-};
-
-export const TCP_CONGESTION_OPTION = {
-    BBR: "bbr",
-    CUBIC: "cubic",
-    RENO: "reno",
-};
-
-export const USERS_SECURITY = {
-    AES_128_GCM: "aes-128-gcm",
-    CHACHA20_POLY1305: "chacha20-poly1305",
-    AUTO: "auto",
-    NONE: "none",
-    ZERO: "zero",
-};
-
-export const MODE_OPTION = {
-    AUTO: "auto",
-    PACKET_UP: "packet-up",
-    STREAM_UP: "stream-up",
-    STREAM_ONE: "stream-one",
-};
-
-Object.freeze(Protocols);
-Object.freeze(SSMethods);
-Object.freeze(TLS_FLOW_CONTROL);
-Object.freeze(TLS_VERSION_OPTION);
-Object.freeze(TLS_CIPHER_OPTION);
-Object.freeze(UTLS_FINGERPRINT);
-Object.freeze(ALPN_OPTION);
-Object.freeze(SNIFFING_OPTION);
-Object.freeze(USAGE_OPTION);
-Object.freeze(DOMAIN_STRATEGY_OPTION);
-Object.freeze(TCP_CONGESTION_OPTION);
-Object.freeze(USERS_SECURITY);
-Object.freeze(MODE_OPTION);
-
-export type JsonObject = Record<string, unknown>;
-export interface HeaderEntry { name: string; value: string }
-export interface FallbackEntry {
-    dest?: string | number;
-    name?: string;
-    alpn?: string;
-    path?: string;
-    xver?: number | string;
-}
-
-export class XrayCommonClass {
-    [key: string]: any;
-
-    static toJsonArray<T extends { toJson(): unknown }>(arr: T[]): unknown[] {
-        return arr.map((obj) => obj.toJson());
-    }
-
-    static fromJson(..._args: unknown[]): XrayCommonClass | undefined {
-        return new XrayCommonClass();
-    }
-
-    toJson(): unknown {
-        return this;
-    }
-
-    static fallbackToJson(fb: FallbackEntry): JsonObject {
-        const out: JsonObject = { dest: fb.dest };
-        if (fb.name) out.name = fb.name;
-        if (fb.alpn) out.alpn = fb.alpn;
-        if (fb.path) out.path = fb.path;
-        const xver = Number(fb.xver);
-        if (Number.isInteger(xver) && xver > 0) out.xver = xver;
-        return out;
-    }
-
-    toString(format: boolean = true): string {
-        return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
-    }
-
-    static toHeaders(v2Headers: unknown): HeaderEntry[] {
-        const newHeaders: HeaderEntry[] = [];
-        if (v2Headers && typeof v2Headers === 'object') {
-            const map = v2Headers as Record<string, string | string[]>;
-            Object.keys(map).forEach((key: string) => {
-                const values = map[key];
-                if (typeof values === 'string') {
-                    newHeaders.push({ name: key, value: values });
-                } else if (Array.isArray(values)) {
-                    for (let i = 0; i < values.length; ++i) {
-                        newHeaders.push({ name: key, value: values[i] });
-                    }
-                }
-            });
-        }
-        return newHeaders;
-    }
-
-    static toV2Headers(headers: HeaderEntry[], arr: boolean = true): Record<string, string | string[]> {
-        const v2Headers: Record<string, string | string[]> = {};
-        for (let i = 0; i < headers.length; ++i) {
-            const name = headers[i].name;
-            const value = headers[i].value;
-            if (ObjectUtil.isEmpty(name) || ObjectUtil.isEmpty(value)) {
-                continue;
-            }
-            if (!(name in v2Headers)) {
-                v2Headers[name] = arr ? [value] : value;
-            } else {
-                const existing = v2Headers[name];
-                if (arr && Array.isArray(existing)) {
-                    existing.push(value);
-                } else {
-                    v2Headers[name] = value;
-                }
-            }
-        }
-        return v2Headers;
-    }
-}
-
-export class TcpStreamSettings extends XrayCommonClass {
-    static TcpRequest: any;
-    static TcpResponse: any;
-
-    constructor(
-        acceptProxyProtocol: any = false,
-        type: any = 'none',
-        request: any = new TcpStreamSettings.TcpRequest(),
-        response = new TcpStreamSettings.TcpResponse(),
-    ) {
-        super();
-        this.acceptProxyProtocol = acceptProxyProtocol;
-        this.type = type;
-        this.request = request;
-        this.response = response;
-    }
-
-    static fromJson(json: any = {}) {
-        let header = json.header;
-        if (!header) {
-            header = {};
-        }
-        return new TcpStreamSettings(json.acceptProxyProtocol,
-            header.type,
-            TcpStreamSettings.TcpRequest.fromJson(header.request),
-            TcpStreamSettings.TcpResponse.fromJson(header.response),
-        );
-    }
-
-    toJson() {
-        const json: any = {};
-        if (this.acceptProxyProtocol) {
-            json.acceptProxyProtocol = true;
-        }
-        if (this.type === 'http') {
-            json.header = {
-                type: 'http',
-                request: this.request.toJson(),
-                response: this.response.toJson(),
-            };
-        } else if (this.type && this.type !== 'none') {
-            json.header = { type: this.type };
-        }
-        return json;
-    }
-}
-
-TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
-    constructor(
-        version = '1.1',
-        method = 'GET',
-        path = ['/'],
-        headers: any[] = [],
-    ) {
-        super();
-        this.version = version;
-        this.method = method;
-        this.path = path.length === 0 ? ['/'] : path;
-        this.headers = headers;
-    }
-
-    addPath(path: any) {
-        this.path.push(path);
-    }
-
-    removePath(index: number) {
-        this.path.splice(index, 1);
-    }
-
-    addHeader(name: any, value: any) {
-        this.headers.push({ name: name, value: value });
-    }
-
-    removeHeader(index: number) {
-        this.headers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new TcpStreamSettings.TcpRequest(
-            json.version,
-            json.method,
-            json.path,
-            XrayCommonClass.toHeaders(json.headers),
-        );
-    }
-
-    toJson() {
-        return {
-            version: this.version,
-            method: this.method,
-            path: ObjectUtil.clone(this.path),
-            headers: XrayCommonClass.toV2Headers(this.headers),
-        };
-    }
-};
-
-TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
-    constructor(
-        version = '1.1',
-        status = '200',
-        reason = 'OK',
-        headers: any[] = [],
-    ) {
-        super();
-        this.version = version;
-        this.status = status;
-        this.reason = reason;
-        this.headers = headers;
-    }
-
-    addHeader(name: any, value: any) {
-        this.headers.push({ name: name, value: value });
-    }
-
-    removeHeader(index: number) {
-        this.headers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new TcpStreamSettings.TcpResponse(
-            json.version,
-            json.status,
-            json.reason,
-            XrayCommonClass.toHeaders(json.headers),
-        );
-    }
-
-    toJson() {
-        return {
-            version: this.version,
-            status: this.status,
-            reason: this.reason,
-            headers: XrayCommonClass.toV2Headers(this.headers),
-        };
-    }
-};
-
-export class KcpStreamSettings extends XrayCommonClass {
-    constructor(
-        mtu = 1350,
-        tti = 20,
-        uplinkCapacity = 5,
-        downlinkCapacity = 20,
-        cwndMultiplier = 1,
-        maxSendingWindow = 2097152,
-    ) {
-        super();
-        this.mtu = mtu;
-        this.tti = tti;
-        this.upCap = uplinkCapacity;
-        this.downCap = downlinkCapacity;
-        this.cwndMultiplier = cwndMultiplier;
-        this.maxSendingWindow = maxSendingWindow;
-    }
-
-    static fromJson(json: any = {}) {
-        return new KcpStreamSettings(
-            json.mtu,
-            json.tti,
-            json.uplinkCapacity,
-            json.downlinkCapacity,
-            json.cwndMultiplier,
-            json.maxSendingWindow,
-        );
-    }
-
-    toJson() {
-        return {
-            mtu: this.mtu,
-            tti: this.tti,
-            uplinkCapacity: this.upCap,
-            downlinkCapacity: this.downCap,
-            cwndMultiplier: this.cwndMultiplier,
-            maxSendingWindow: this.maxSendingWindow,
-        };
-    }
-}
-
-export class WsStreamSettings extends XrayCommonClass {
-    constructor(
-        acceptProxyProtocol: any = false,
-        path = '/',
-        host = '',
-        headers: any[] = [],
-        heartbeatPeriod = 0,
-    ) {
-        super();
-        this.acceptProxyProtocol = acceptProxyProtocol;
-        this.path = path;
-        this.host = host;
-        this.headers = headers;
-        this.heartbeatPeriod = heartbeatPeriod;
-    }
-
-    addHeader(name: any, value: any) {
-        this.headers.push({ name: name, value: value });
-    }
-
-    removeHeader(index: number) {
-        this.headers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new WsStreamSettings(
-            json.acceptProxyProtocol,
-            json.path,
-            json.host,
-            XrayCommonClass.toHeaders(json.headers),
-            json.heartbeatPeriod,
-        );
-    }
-
-    toJson() {
-        return {
-            acceptProxyProtocol: this.acceptProxyProtocol,
-            path: this.path,
-            host: this.host,
-            headers: XrayCommonClass.toV2Headers(this.headers, false),
-            heartbeatPeriod: this.heartbeatPeriod,
-        };
-    }
-}
-
-export class GrpcStreamSettings extends XrayCommonClass {
-    constructor(
-        serviceName = "",
-        authority = "",
-        multiMode = false,
-    ) {
-        super();
-        this.serviceName = serviceName;
-        this.authority = authority;
-        this.multiMode = multiMode;
-    }
-
-    static fromJson(json: any = {}) {
-        return new GrpcStreamSettings(
-            json.serviceName,
-            json.authority,
-            json.multiMode
-        );
-    }
-
-    toJson() {
-        return {
-            serviceName: this.serviceName,
-            authority: this.authority,
-            multiMode: this.multiMode,
-        }
-    }
-}
-
-export class HTTPUpgradeStreamSettings extends XrayCommonClass {
-    constructor(
-        acceptProxyProtocol: any = false,
-        path = '/',
-        host = '',
-        headers: any[] = []
-    ) {
-        super();
-        this.acceptProxyProtocol = acceptProxyProtocol;
-        this.path = path;
-        this.host = host;
-        this.headers = headers;
-    }
-
-    addHeader(name: any, value: any) {
-        this.headers.push({ name: name, value: value });
-    }
-
-    removeHeader(index: number) {
-        this.headers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new HTTPUpgradeStreamSettings(
-            json.acceptProxyProtocol,
-            json.path,
-            json.host,
-            XrayCommonClass.toHeaders(json.headers),
-        );
-    }
-
-    toJson() {
-        return {
-            acceptProxyProtocol: this.acceptProxyProtocol,
-            path: this.path,
-            host: this.host,
-            headers: XrayCommonClass.toV2Headers(this.headers, false),
-        };
-    }
-}
-
-// Mirrors the inbound (server-side) view of Xray-core's SplitHTTPConfig
-// (infra/conf/transport_internet.go). Only fields the server actually
-// reads at runtime, plus the bidirectional fields the server enforces,
-// live here. Most client-only fields (uplinkChunkSize, noGRPCHeader,
-// scMinPostsIntervalMs, xmux, downloadSettings) belong on the outbound
-// class instead.
-//
-// `headers` and `uplinkHTTPMethod` are client-only at runtime (xray's
-// listener doesn't read them) but we keep them here so the admin can set
-// values that get embedded into the share link's `extra` blob.
-export class xHTTPStreamSettings extends XrayCommonClass {
-    constructor(
-        // Bidirectional — must match between client and server
-        path = '/',
-        host = '',
-        mode = MODE_OPTION.AUTO,
-        xPaddingBytes = "100-1000",
-        xPaddingObfsMode = false,
-        xPaddingKey = '',
-        xPaddingHeader = '',
-        xPaddingPlacement = '',
-        xPaddingMethod = '',
-        sessionPlacement = '',
-        sessionKey = '',
-        seqPlacement = '',
-        seqKey = '',
-        uplinkDataPlacement = '',
-        uplinkDataKey = '',
-        scMaxEachPostBytes = "1000000",
-        // Server-side only
-        noSSEHeader = false,
-        scMaxBufferedPosts = 30,
-        scStreamUpServerSecs = "20-80",
-        serverMaxHeaderBytes = 0,
-        // URL-share only — embedded in the link's `extra` blob so clients
-        // pick them up; xray's listener ignores them at runtime.
-        uplinkHTTPMethod = '',
-        headers: any[] = [],
-    ) {
-        super();
-        this.path = path;
-        this.host = host;
-        this.mode = mode;
-        this.xPaddingBytes = xPaddingBytes;
-        this.xPaddingObfsMode = xPaddingObfsMode;
-        this.xPaddingKey = xPaddingKey;
-        this.xPaddingHeader = xPaddingHeader;
-        this.xPaddingPlacement = xPaddingPlacement;
-        this.xPaddingMethod = xPaddingMethod;
-        this.sessionPlacement = sessionPlacement;
-        this.sessionKey = sessionKey;
-        this.seqPlacement = seqPlacement;
-        this.seqKey = seqKey;
-        this.uplinkDataPlacement = uplinkDataPlacement;
-        this.uplinkDataKey = uplinkDataKey;
-        this.scMaxEachPostBytes = scMaxEachPostBytes;
-        this.noSSEHeader = noSSEHeader;
-        this.scMaxBufferedPosts = scMaxBufferedPosts;
-        this.scStreamUpServerSecs = scStreamUpServerSecs;
-        this.serverMaxHeaderBytes = serverMaxHeaderBytes;
-        this.uplinkHTTPMethod = uplinkHTTPMethod;
-        this.headers = headers;
-    }
-
-    addHeader(name: any, value: any) {
-        this.headers.push({ name: name, value: value });
-    }
-
-    removeHeader(index: number) {
-        this.headers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new xHTTPStreamSettings(
-            json.path,
-            json.host,
-            json.mode,
-            json.xPaddingBytes,
-            json.xPaddingObfsMode,
-            json.xPaddingKey,
-            json.xPaddingHeader,
-            json.xPaddingPlacement,
-            json.xPaddingMethod,
-            json.sessionPlacement,
-            json.sessionKey,
-            json.seqPlacement,
-            json.seqKey,
-            json.uplinkDataPlacement,
-            json.uplinkDataKey,
-            json.scMaxEachPostBytes,
-            json.noSSEHeader,
-            json.scMaxBufferedPosts,
-            json.scStreamUpServerSecs,
-            json.serverMaxHeaderBytes,
-            json.uplinkHTTPMethod,
-            XrayCommonClass.toHeaders(json.headers),
-        );
-    }
-
-    toJson() {
-        return {
-            path: this.path,
-            host: this.host,
-            mode: this.mode,
-            xPaddingBytes: this.xPaddingBytes,
-            xPaddingObfsMode: this.xPaddingObfsMode,
-            xPaddingKey: this.xPaddingKey,
-            xPaddingHeader: this.xPaddingHeader,
-            xPaddingPlacement: this.xPaddingPlacement,
-            xPaddingMethod: this.xPaddingMethod,
-            sessionPlacement: this.sessionPlacement,
-            sessionKey: this.sessionKey,
-            seqPlacement: this.seqPlacement,
-            seqKey: this.seqKey,
-            uplinkDataPlacement: this.uplinkDataPlacement,
-            uplinkDataKey: this.uplinkDataKey,
-            scMaxEachPostBytes: this.scMaxEachPostBytes,
-            noSSEHeader: this.noSSEHeader,
-            scMaxBufferedPosts: this.scMaxBufferedPosts,
-            scStreamUpServerSecs: this.scStreamUpServerSecs,
-            serverMaxHeaderBytes: this.serverMaxHeaderBytes,
-            uplinkHTTPMethod: this.uplinkHTTPMethod,
-            headers: XrayCommonClass.toV2Headers(this.headers, false),
-        };
-    }
-}
-
-export class HysteriaStreamSettings extends XrayCommonClass {
-    constructor(
-        protocol?: any,
-        version: any = 2,
-        auth: any = '',
-        udpIdleTimeout: any = 60,
-        masquerade?: any,
-    ) {
-        super();
-        this.protocol = protocol;
-        this.version = version;
-        this.auth = auth;
-        this.udpIdleTimeout = udpIdleTimeout;
-        this.masquerade = masquerade;
-    }
-
-    static fromJson(json: any = {}) {
-        return new HysteriaStreamSettings(
-            json.protocol,
-            json.version ?? 2,
-            json.auth ?? '',
-            json.udpIdleTimeout ?? 60,
-            json.masquerade ? HysteriaMasquerade.fromJson(json.masquerade) : undefined,
-        );
-    }
-
-    toJson() {
-        return {
-            protocol: this.protocol,
-            version: this.version,
-            auth: this.auth,
-            udpIdleTimeout: this.udpIdleTimeout,
-            masquerade: this.masqueradeSwitch ? this.masquerade.toJson() : undefined,
-        };
-    }
-
-    get masqueradeSwitch() {
-        return this.masquerade != undefined;
-    }
-
-    set masqueradeSwitch(value) {
-        this.masquerade = value ? new HysteriaMasquerade() : undefined;
-    }
-};
-
-export class HysteriaMasquerade extends XrayCommonClass {
-    constructor(
-        type = 'proxy',
-        dir = '',
-        url = '',
-        rewriteHost = false,
-        insecure = false,
-        content = '',
-        headers: any[] = [],
-        statusCode = 0,
-    ) {
-        super();
-        this.type = type;
-        this.dir = dir;
-        this.url = url;
-        this.rewriteHost = rewriteHost;
-        this.insecure = insecure;
-        this.content = content;
-        this.headers = headers;
-        this.statusCode = statusCode;
-    }
-
-    addHeader(name: any, value: any) {
-        this.headers.push({ name: name, value: value });
-    }
-
-    removeHeader(index: number) {
-        this.headers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        const type = ['proxy', 'file', 'string'].includes(json.type) ? json.type : 'proxy';
-        return new HysteriaMasquerade(
-            type,
-            json.dir,
-            json.url,
-            json.rewriteHost,
-            json.insecure,
-            json.content,
-            XrayCommonClass.toHeaders(json.headers),
-            json.statusCode,
-        );
-    }
-
-    toJson() {
-        return {
-            type: this.type,
-            dir: this.dir,
-            url: this.url,
-            rewriteHost: this.rewriteHost,
-            insecure: this.insecure,
-            content: this.content,
-            headers: XrayCommonClass.toV2Headers(this.headers, false),
-            statusCode: this.statusCode,
-        };
-    }
-};
-export class TlsStreamSettings extends XrayCommonClass {
-    static Cert: any;
-    static Settings: any;
-
-    constructor(
-        serverName: any = '',
-        minVersion = TLS_VERSION_OPTION.TLS12,
-        maxVersion = TLS_VERSION_OPTION.TLS13,
-        cipherSuites = '',
-        rejectUnknownSni = false,
-        disableSystemRoot = false,
-        enableSessionResumption = false,
-        certificates = [new TlsStreamSettings.Cert()],
-        alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
-        echServerKeys = '',
-        settings = new TlsStreamSettings.Settings()
-    ) {
-        super();
-        this.sni = serverName;
-        this.minVersion = minVersion;
-        this.maxVersion = maxVersion;
-        this.cipherSuites = cipherSuites;
-        this.rejectUnknownSni = rejectUnknownSni;
-        this.disableSystemRoot = disableSystemRoot;
-        this.enableSessionResumption = enableSessionResumption;
-        this.certs = certificates;
-        this.alpn = alpn;
-        this.echServerKeys = echServerKeys;
-        this.settings = settings;
-    }
-
-    addCert() {
-        this.certs.push(new TlsStreamSettings.Cert());
-    }
-
-    removeCert(index: number) {
-        this.certs.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        let certs;
-        let settings;
-        if (!ObjectUtil.isEmpty(json.certificates)) {
-            certs = json.certificates.map((cert: any) => TlsStreamSettings.Cert.fromJson(cert));
-        }
-
-        if (!ObjectUtil.isEmpty(json.settings)) {
-            settings = new TlsStreamSettings.Settings(json.settings.fingerprint, json.settings.echConfigList);
-        }
-        return new TlsStreamSettings(
-            json.serverName,
-            json.minVersion,
-            json.maxVersion,
-            json.cipherSuites,
-            json.rejectUnknownSni,
-            json.disableSystemRoot,
-            json.enableSessionResumption,
-            certs,
-            json.alpn,
-            json.echServerKeys,
-            settings,
-        );
-    }
-
-    toJson() {
-        return {
-            serverName: this.sni,
-            minVersion: this.minVersion,
-            maxVersion: this.maxVersion,
-            cipherSuites: this.cipherSuites,
-            rejectUnknownSni: this.rejectUnknownSni,
-            disableSystemRoot: this.disableSystemRoot,
-            enableSessionResumption: this.enableSessionResumption,
-            certificates: TlsStreamSettings.toJsonArray(this.certs),
-            alpn: this.alpn,
-            echServerKeys: this.echServerKeys,
-            settings: this.settings,
-        };
-    }
-}
-
-TlsStreamSettings.Cert = class extends XrayCommonClass {
-    constructor(
-        useFile = true,
-        certificateFile = '',
-        keyFile = '',
-        certificate = '',
-        key = '',
-        oneTimeLoading = false,
-        usage = USAGE_OPTION.ENCIPHERMENT,
-        buildChain = false,
-    ) {
-        super();
-        this.useFile = useFile;
-        this.certFile = certificateFile;
-        this.keyFile = keyFile;
-        this.cert = Array.isArray(certificate) ? certificate.join('\n') : certificate;
-        this.key = Array.isArray(key) ? key.join('\n') : key;
-        this.oneTimeLoading = oneTimeLoading;
-        this.usage = usage;
-        this.buildChain = buildChain
-    }
-
-    static fromJson(json: any = {}) {
-        if ('certificateFile' in json && 'keyFile' in json) {
-            return new TlsStreamSettings.Cert(
-                true,
-                json.certificateFile,
-                json.keyFile, '', '',
-                json.oneTimeLoading,
-                json.usage,
-                json.buildChain,
-            );
-        } else {
-            return new TlsStreamSettings.Cert(
-                false, '', '',
-                Array.isArray(json.certificate) ? json.certificate.join('\n') : (json.certificate ?? ''),
-                Array.isArray(json.key) ? json.key.join('\n') : (json.key ?? ''),
-                json.oneTimeLoading,
-                json.usage,
-                json.buildChain,
-            );
-        }
-    }
-
-    toJson() {
-        if (this.useFile) {
-            return {
-                certificateFile: this.certFile,
-                keyFile: this.keyFile,
-                oneTimeLoading: this.oneTimeLoading,
-                usage: this.usage,
-                buildChain: this.buildChain,
-            };
-        } else {
-            return {
-                certificate: this.cert.split('\n'),
-                key: this.key.split('\n'),
-                oneTimeLoading: this.oneTimeLoading,
-                usage: this.usage,
-                buildChain: this.buildChain,
-            };
-        }
-    }
-};
-
-TlsStreamSettings.Settings = class extends XrayCommonClass {
-    constructor(
-        fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
-        echConfigList = '',
-    ) {
-        super();
-        this.fingerprint = fingerprint;
-        this.echConfigList = echConfigList;
-    }
-    static fromJson(json: any = {}) {
-        return new TlsStreamSettings.Settings(
-            json.fingerprint,
-            json.echConfigList,
-        );
-    }
-    toJson() {
-        return {
-            fingerprint: this.fingerprint,
-            echConfigList: this.echConfigList
-        };
-    }
-};
-
-
-export class RealityStreamSettings extends XrayCommonClass {
-    static Settings: any;
-
-    constructor(
-        show: any = false,
-        xver = 0,
-        target = '',
-        serverNames = '',
-        privateKey = '',
-        minClientVer = '',
-        maxClientVer = '',
-        maxTimediff = 0,
-        shortIds = RandomUtil.randomShortIds(),
-        mldsa65Seed = '',
-        settings = new RealityStreamSettings.Settings()
-    ) {
-        super();
-        // If target/serverNames are not provided, use random values
-        if (!target && !serverNames) {
-            const randomTarget = getRandomRealityTarget();
-            target = randomTarget.target;
-            serverNames = randomTarget.sni;
-        }
-        this.show = show;
-        this.xver = xver;
-        this.target = target;
-        this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
-        this.privateKey = privateKey;
-        this.minClientVer = minClientVer;
-        this.maxClientVer = maxClientVer;
-        this.maxTimediff = maxTimediff;
-        this.shortIds = Array.isArray(shortIds) ? shortIds.join(",") : shortIds;
-        this.mldsa65Seed = mldsa65Seed;
-        this.settings = settings;
-    }
-
-    static fromJson(json: any = {}) {
-        let settings;
-        if (!ObjectUtil.isEmpty(json.settings)) {
-            settings = new RealityStreamSettings.Settings(
-                json.settings.publicKey,
-                json.settings.fingerprint,
-                json.settings.serverName,
-                json.settings.spiderX,
-                json.settings.mldsa65Verify,
-            );
-        }
-        return new RealityStreamSettings(
-            json.show,
-            json.xver,
-            json.target,
-            json.serverNames,
-            json.privateKey,
-            json.minClientVer,
-            json.maxClientVer,
-            json.maxTimediff,
-            json.shortIds,
-            json.mldsa65Seed,
-            settings,
-        );
-    }
-
-    toJson() {
-        return {
-            show: this.show,
-            xver: this.xver,
-            target: this.target,
-            serverNames: this.serverNames.split(","),
-            privateKey: this.privateKey,
-            minClientVer: this.minClientVer,
-            maxClientVer: this.maxClientVer,
-            maxTimediff: this.maxTimediff,
-            shortIds: this.shortIds.split(","),
-            mldsa65Seed: this.mldsa65Seed,
-            settings: this.settings,
-        };
-    }
-}
-
-RealityStreamSettings.Settings = class extends XrayCommonClass {
-    constructor(
-        publicKey = '',
-        fingerprint = UTLS_FINGERPRINT.UTLS_CHROME,
-        serverName = '',
-        spiderX = '/',
-        mldsa65Verify = ''
-    ) {
-        super();
-        this.publicKey = publicKey;
-        this.fingerprint = fingerprint;
-        this.serverName = serverName;
-        this.spiderX = spiderX;
-        this.mldsa65Verify = mldsa65Verify;
-    }
-    static fromJson(json: any = {}) {
-        return new RealityStreamSettings.Settings(
-            json.publicKey,
-            json.fingerprint,
-            json.serverName,
-            json.spiderX,
-            json.mldsa65Verify
-        );
-    }
-    toJson() {
-        return {
-            publicKey: this.publicKey,
-            fingerprint: this.fingerprint,
-            serverName: this.serverName,
-            spiderX: this.spiderX,
-            mldsa65Verify: this.mldsa65Verify
-        };
-    }
-};
-
-export class SockoptStreamSettings extends XrayCommonClass {
-    constructor(
-        acceptProxyProtocol: any = false,
-        tcpFastOpen = false,
-        mark = 0,
-        tproxy = "off",
-        tcpMptcp = false,
-        penetrate = false,
-        domainStrategy = DOMAIN_STRATEGY_OPTION.USE_IP,
-        tcpMaxSeg = 1440,
-        dialerProxy = "",
-        tcpKeepAliveInterval = 0,
-        tcpKeepAliveIdle = 300,
-        tcpUserTimeout = 10000,
-        tcpcongestion = TCP_CONGESTION_OPTION.BBR,
-        V6Only = false,
-        tcpWindowClamp = 600,
-        interfaceName = "",
-        trustedXForwardedFor = [],
-    ) {
-        super();
-        this.acceptProxyProtocol = acceptProxyProtocol;
-        this.tcpFastOpen = tcpFastOpen;
-        this.mark = mark;
-        this.tproxy = tproxy;
-        this.tcpMptcp = tcpMptcp;
-        this.penetrate = penetrate;
-        this.domainStrategy = domainStrategy;
-        this.tcpMaxSeg = tcpMaxSeg;
-        this.dialerProxy = dialerProxy;
-        this.tcpKeepAliveInterval = tcpKeepAliveInterval;
-        this.tcpKeepAliveIdle = tcpKeepAliveIdle;
-        this.tcpUserTimeout = tcpUserTimeout;
-        this.tcpcongestion = tcpcongestion;
-        this.V6Only = V6Only;
-        this.tcpWindowClamp = tcpWindowClamp;
-        this.interfaceName = interfaceName;
-        this.trustedXForwardedFor = trustedXForwardedFor;
-    }
-
-    static fromJson(json: any = {}) {
-        if (Object.keys(json).length === 0) return undefined;
-        return new SockoptStreamSettings(
-            json.acceptProxyProtocol,
-            json.tcpFastOpen,
-            json.mark,
-            json.tproxy,
-            json.tcpMptcp,
-            json.penetrate,
-            json.domainStrategy,
-            json.tcpMaxSeg,
-            json.dialerProxy,
-            json.tcpKeepAliveInterval,
-            json.tcpKeepAliveIdle,
-            json.tcpUserTimeout,
-            json.tcpcongestion,
-            json.V6Only,
-            json.tcpWindowClamp,
-            json.interface,
-            json.trustedXForwardedFor || [],
-        );
-    }
-
-    toJson() {
-        const result: any = {
-            acceptProxyProtocol: this.acceptProxyProtocol,
-            tcpFastOpen: this.tcpFastOpen,
-            mark: this.mark,
-            tproxy: this.tproxy,
-            tcpMptcp: this.tcpMptcp,
-            penetrate: this.penetrate,
-            domainStrategy: this.domainStrategy,
-            tcpMaxSeg: this.tcpMaxSeg,
-            dialerProxy: this.dialerProxy,
-            tcpKeepAliveInterval: this.tcpKeepAliveInterval,
-            tcpKeepAliveIdle: this.tcpKeepAliveIdle,
-            tcpUserTimeout: this.tcpUserTimeout,
-            tcpcongestion: this.tcpcongestion,
-            V6Only: this.V6Only,
-            tcpWindowClamp: this.tcpWindowClamp,
-            interface: this.interfaceName,
-        };
-        if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
-            result.trustedXForwardedFor = this.trustedXForwardedFor;
-        }
-        return result;
-    }
-}
-
-export class UdpMask extends XrayCommonClass {
-    constructor(type: any = 'salamander', settings: any = {}) {
-        super();
-        this.type = type;
-        this.settings = this._getDefaultSettings(type, settings);
-    }
-
-    _getDefaultSettings(type: any, settings: any = {}): any {
-        switch (type) {
-            case 'salamander':
-            case 'mkcp-aes128gcm':
-                return { password: settings.password || '' };
-            case 'header-dns':
-                return { domain: settings.domain || '' };
-            case 'xdns':
-                return { domains: Array.isArray(settings.domains) ? settings.domains : [] };
-            case 'xicmp':
-                return { ip: settings.ip || '', id: settings.id ?? 0 };
-            case 'mkcp-original':
-            case 'header-dtls':
-            case 'header-srtp':
-            case 'header-utp':
-            case 'header-wechat':
-            case 'header-wireguard':
-                return {};
-            case 'header-custom':
-                return {
-                    client: Array.isArray(settings.client) ? settings.client : [],
-                    server: Array.isArray(settings.server) ? settings.server : [],
-                };
-            case 'noise':
-                return {
-                    reset: settings.reset ?? 0,
-                    noise: Array.isArray(settings.noise) ? settings.noise : [],
-                };
-            default:
-                return settings;
-        }
-    }
-
-    static fromJson(json: any = {}) {
-        return new UdpMask(
-            json.type || 'salamander',
-            json.settings || {}
-        );
-    }
-
-    toJson() {
-        const cleanItem = (item: any) => {
-            const out = { ...item };
-            if (out.type === 'array') {
-                delete out.packet;
-            } else {
-                delete out.rand;
-                delete out.randRange;
-            }
-            return out;
-        };
-
-        let settings = this.settings;
-        if (this.type === 'noise' && settings && Array.isArray(settings.noise)) {
-            settings = { ...settings, noise: settings.noise.map(cleanItem) };
-        } else if (this.type === 'header-custom' && settings) {
-            settings = {
-                ...settings,
-                client: Array.isArray(settings.client) ? settings.client.map(cleanItem) : settings.client,
-                server: Array.isArray(settings.server) ? settings.server.map(cleanItem) : settings.server,
-            };
-        }
-
-        return {
-            type: this.type,
-            settings: (settings && Object.keys(settings).length > 0) ? settings : undefined
-        };
-    }
-}
-
-export class TcpMask extends XrayCommonClass {
-    constructor(type: any = 'fragment', settings: any = {}) {
-        super();
-        this.type = type;
-        this.settings = this._getDefaultSettings(type, settings);
-    }
-
-    _getDefaultSettings(type: any, settings: any = {}): any {
-        switch (type) {
-            case 'fragment':
-                return {
-                    packets: settings.packets ?? 'tlshello',
-                    length: settings.length ?? '',
-                    delay: settings.delay ?? '',
-                    maxSplit: settings.maxSplit ?? '',
-                };
-            case 'sudoku':
-                return {
-                    password: settings.password ?? '',
-                    ascii: settings.ascii ?? '',
-                    customTable: settings.customTable ?? '',
-                    customTables: Array.isArray(settings.customTables) ? settings.customTables : [],
-                    paddingMin: settings.paddingMin ?? 0,
-                    paddingMax: settings.paddingMax ?? 0,
-                };
-            case 'header-custom':
-                return {
-                    clients: Array.isArray(settings.clients) ? settings.clients : [],
-                    servers: Array.isArray(settings.servers) ? settings.servers : [],
-                };
-            default:
-                return settings;
-        }
-    }
-
-    static fromJson(json: any = {}) {
-        return new TcpMask(
-            json.type || 'fragment',
-            json.settings || {}
-        );
-    }
-
-    toJson() {
-        const cleanItem = (item: any) => {
-            const out = { ...item };
-            if (out.type === 'array') {
-                delete out.packet;
-            } else {
-                delete out.rand;
-                delete out.randRange;
-            }
-            return out;
-        };
-
-        let settings = this.settings;
-        if (this.type === 'header-custom' && settings) {
-            const cleanGroup = (group: any) => Array.isArray(group) ? group.map(cleanItem) : group;
-            settings = {
-                ...settings,
-                clients: Array.isArray(settings.clients) ? settings.clients.map(cleanGroup) : settings.clients,
-                servers: Array.isArray(settings.servers) ? settings.servers.map(cleanGroup) : settings.servers,
-            };
-        }
-
-        return {
-            type: this.type,
-            settings: (settings && Object.keys(settings).length > 0) ? settings : undefined
-        };
-    }
-}
-
-export class QuicParams extends XrayCommonClass {
-    constructor(
-        congestion: any = 'bbr',
-        debug: any = false,
-        brutalUp: any = 65537,
-        brutalDown: any = 65537,
-        udpHop: any = undefined,
-        initStreamReceiveWindow: any = 8388608,
-        maxStreamReceiveWindow: any = 8388608,
-        initConnectionReceiveWindow: any = 20971520,
-        maxConnectionReceiveWindow: any = 20971520,
-        maxIdleTimeout: any = 30,
-        keepAlivePeriod: any = 5,
-        disablePathMTUDiscovery: any = false,
-        maxIncomingStreams = 1024,
-    ) {
-        super();
-        this.congestion = congestion;
-        this.debug = debug;
-        this.brutalUp = brutalUp;
-        this.brutalDown = brutalDown;
-        this.udpHop = udpHop;
-        this.initStreamReceiveWindow = initStreamReceiveWindow;
-        this.maxStreamReceiveWindow = maxStreamReceiveWindow;
-        this.initConnectionReceiveWindow = initConnectionReceiveWindow;
-        this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
-        this.maxIdleTimeout = maxIdleTimeout;
-        this.keepAlivePeriod = keepAlivePeriod;
-        this.disablePathMTUDiscovery = disablePathMTUDiscovery;
-        this.maxIncomingStreams = maxIncomingStreams;
-    }
-
-    get hasUdpHop() {
-        return this.udpHop != null;
-    }
-
-    set hasUdpHop(value) {
-        this.udpHop = value ? (this.udpHop || { ports: '20000-50000', interval: '5-10' }) : undefined;
-    }
-
-    static fromJson(json: any = {}) {
-        if (!json || Object.keys(json).length === 0) return undefined;
-        return new QuicParams(
-            json.congestion,
-            json.debug,
-            json.brutalUp,
-            json.brutalDown,
-            json.udpHop ? { ports: json.udpHop.ports, interval: json.udpHop.interval } : undefined,
-            json.initStreamReceiveWindow,
-            json.maxStreamReceiveWindow,
-            json.initConnectionReceiveWindow,
-            json.maxConnectionReceiveWindow,
-            json.maxIdleTimeout,
-            json.keepAlivePeriod,
-            json.disablePathMTUDiscovery,
-            json.maxIncomingStreams,
-        );
-    }
-
-    toJson() {
-        const result: any = { congestion: this.congestion };
-        if (this.debug) result.debug = this.debug;
-        if (['brutal', 'force-brutal'].includes(this.congestion)) {
-            if (this.brutalUp) result.brutalUp = this.brutalUp;
-            if (this.brutalDown) result.brutalDown = this.brutalDown;
-        }
-        if (this.udpHop) result.udpHop = { ports: this.udpHop.ports, interval: this.udpHop.interval };
-        if (this.initStreamReceiveWindow > 0) result.initStreamReceiveWindow = this.initStreamReceiveWindow;
-        if (this.maxStreamReceiveWindow > 0) result.maxStreamReceiveWindow = this.maxStreamReceiveWindow;
-        if (this.initConnectionReceiveWindow > 0) result.initConnectionReceiveWindow = this.initConnectionReceiveWindow;
-        if (this.maxConnectionReceiveWindow > 0) result.maxConnectionReceiveWindow = this.maxConnectionReceiveWindow;
-        if (this.maxIdleTimeout !== 30 && this.maxIdleTimeout > 0) result.maxIdleTimeout = this.maxIdleTimeout;
-        if (this.keepAlivePeriod > 0) result.keepAlivePeriod = this.keepAlivePeriod;
-        if (this.disablePathMTUDiscovery) result.disablePathMTUDiscovery = this.disablePathMTUDiscovery;
-        if (this.maxIncomingStreams > 0) result.maxIncomingStreams = this.maxIncomingStreams;
-        return result;
-    }
-}
-
-export class FinalMaskStreamSettings extends XrayCommonClass {
-    constructor(tcp: any[] = [], udp: any[] = [], quicParams: any = undefined) {
-        super();
-        this.tcp = Array.isArray(tcp) ? tcp.map((t: any) => t instanceof TcpMask ? t : new TcpMask(t.type, t.settings)) : [];
-        this.udp = Array.isArray(udp) ? udp.map((u: any) => new UdpMask(u.type, u.settings)) : [new UdpMask((udp as any).type, (udp as any).settings)];
-        this.quicParams = quicParams instanceof QuicParams ? quicParams : (quicParams ? QuicParams.fromJson(quicParams) : undefined);
-    }
-
-    get enableQuicParams() {
-        return this.quicParams != null;
-    }
-
-    set enableQuicParams(value) {
-        this.quicParams = value ? (this.quicParams || new QuicParams()) : undefined;
-    }
-
-    static fromJson(json: any = {}) {
-        return new FinalMaskStreamSettings(
-            json.tcp || [],
-            json.udp || [],
-            json.quicParams ? QuicParams.fromJson(json.quicParams) : undefined,
-        );
-    }
-
-    toJson() {
-        const result: any = {} as any;
-        if (this.tcp && this.tcp.length > 0) {
-            result.tcp = this.tcp.map((t: any) => t.toJson());
-        }
-        if (this.udp && this.udp.length > 0) {
-            result.udp = this.udp.map((udp: any) => udp.toJson());
-        }
-        if (this.quicParams) {
-            result.quicParams = this.quicParams.toJson();
-        }
-        return result;
-    }
-}
-
-export class StreamSettings extends XrayCommonClass {
-    constructor(network = 'tcp',
-        security = 'none',
-        externalProxy = [],
-        tlsSettings = new TlsStreamSettings(),
-        realitySettings = new RealityStreamSettings(),
-        tcpSettings = new TcpStreamSettings(),
-        kcpSettings = new KcpStreamSettings(),
-        wsSettings = new WsStreamSettings(),
-        grpcSettings = new GrpcStreamSettings(),
-        httpupgradeSettings = new HTTPUpgradeStreamSettings(),
-        xhttpSettings = new xHTTPStreamSettings(),
-        hysteriaSettings = new HysteriaStreamSettings(),
-        finalmask = new FinalMaskStreamSettings(),
-        sockopt: any = undefined,
-    ) {
-        super();
-        this.network = network;
-        this.security = security;
-        this.externalProxy = externalProxy;
-        this.tls = tlsSettings;
-        this.reality = realitySettings;
-        this.tcp = tcpSettings;
-        this.kcp = kcpSettings;
-        this.ws = wsSettings;
-        this.grpc = grpcSettings;
-        this.httpupgrade = httpupgradeSettings;
-        this.xhttp = xhttpSettings;
-        this.hysteria = hysteriaSettings;
-        this.finalmask = finalmask;
-        this.sockopt = sockopt;
-    }
-
-    addTcpMask(type = 'fragment') {
-        this.finalmask.tcp.push(new TcpMask(type));
-    }
-
-    delTcpMask(index: number) {
-        if (this.finalmask.tcp) {
-            this.finalmask.tcp.splice(index, 1);
-        }
-    }
-
-    addUdpMask(type = 'salamander') {
-        this.finalmask.udp.push(new UdpMask(type));
-    }
-
-    delUdpMask(index: number) {
-        if (this.finalmask.udp) {
-            this.finalmask.udp.splice(index, 1);
-        }
-    }
-
-    get hasFinalMask() {
-        const hasTcp = this.finalmask.tcp && this.finalmask.tcp.length > 0;
-        const hasUdp = this.finalmask.udp && this.finalmask.udp.length > 0;
-        const hasQuicParams = this.finalmask.quicParams != null;
-        return hasTcp || hasUdp || hasQuicParams;
-    }
-
-    get isTls() {
-        return this.security === "tls";
-    }
-
-    set isTls(isTls) {
-        if (isTls) {
-            this.security = 'tls';
-        } else {
-            this.security = 'none';
-        }
-    }
-
-    //for Reality
-    get isReality() {
-        return this.security === "reality";
-    }
-
-    set isReality(isReality) {
-        if (isReality) {
-            this.security = 'reality';
-        } else {
-            this.security = 'none';
-        }
-    }
-
-    get sockoptSwitch() {
-        return this.sockopt != undefined;
-    }
-
-    set sockoptSwitch(value) {
-        this.sockopt = value ? new SockoptStreamSettings() : undefined;
-    }
-
-    static fromJson(json: any = {}) {
-        return new StreamSettings(
-            json.network,
-            json.security,
-            json.externalProxy,
-            TlsStreamSettings.fromJson(json.tlsSettings),
-            RealityStreamSettings.fromJson(json.realitySettings),
-            TcpStreamSettings.fromJson(json.tcpSettings),
-            KcpStreamSettings.fromJson(json.kcpSettings),
-            WsStreamSettings.fromJson(json.wsSettings),
-            GrpcStreamSettings.fromJson(json.grpcSettings),
-            HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
-            xHTTPStreamSettings.fromJson(json.xhttpSettings),
-            HysteriaStreamSettings.fromJson(json.hysteriaSettings),
-            FinalMaskStreamSettings.fromJson(json.finalmask),
-            SockoptStreamSettings.fromJson(json.sockopt),
-        );
-    }
-
-    toJson() {
-        const network = this.network;
-        return {
-            network: network,
-            security: this.security,
-            externalProxy: Array.isArray(this.externalProxy) && this.externalProxy.length > 0
-                ? this.externalProxy
-                : undefined,
-            tlsSettings: this.isTls ? this.tls.toJson() : undefined,
-            realitySettings: this.isReality ? this.reality.toJson() : undefined,
-            tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
-            kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
-            wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
-            grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
-            httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
-            xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
-            hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
-            finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
-            sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
-        };
-    }
-}
-
-export class Sniffing extends XrayCommonClass {
-    constructor(
-        enabled = false,
-        destOverride = ['http', 'tls', 'quic', 'fakedns'],
-        metadataOnly = false,
-        routeOnly = false,
-        ipsExcluded = [],
-        domainsExcluded = []) {
-        super();
-        this.enabled = enabled;
-        this.destOverride = Array.isArray(destOverride) && destOverride.length > 0 ? destOverride : ['http', 'tls', 'quic', 'fakedns'];
-        this.metadataOnly = metadataOnly;
-        this.routeOnly = routeOnly;
-        this.ipsExcluded = Array.isArray(ipsExcluded) ? ipsExcluded : [];
-        this.domainsExcluded = Array.isArray(domainsExcluded) ? domainsExcluded : [];
-    }
-
-    static fromJson(json: any = {}) {
-        let destOverride = ObjectUtil.clone(json.destOverride);
-        if (ObjectUtil.isEmpty(destOverride) || ObjectUtil.isArrEmpty(destOverride) || ObjectUtil.isEmpty(destOverride[0])) {
-            destOverride = ['http', 'tls', 'quic', 'fakedns'];
-        }
-        return new Sniffing(
-            !!json.enabled,
-            destOverride,
-            json.metadataOnly,
-            json.routeOnly,
-            json.ipsExcluded || [],
-            json.domainsExcluded || [],
-        );
-    }
-
-    toJson() {
-        if (!this.enabled) {
-            return { enabled: false };
-        }
-        return {
-            enabled: true,
-            destOverride: this.destOverride,
-            metadataOnly: this.metadataOnly || undefined,
-            routeOnly: this.routeOnly || undefined,
-            ipsExcluded: this.ipsExcluded.length > 0 ? this.ipsExcluded : undefined,
-            domainsExcluded: this.domainsExcluded.length > 0 ? this.domainsExcluded : undefined,
-        };
-    }
-}
-
-export class Inbound extends XrayCommonClass {
-    static Settings: any;
-    static ClientBase: any;
-    static VmessSettings: any;
-    static VLESSSettings: any;
-    static TrojanSettings: any;
-    static ShadowsocksSettings: any;
-    static HysteriaSettings: any;
-    static TunnelSettings: any;
-    static MixedSettings: any;
-    static HttpSettings: any;
-    static WireguardSettings: any;
-    static TunSettings: any;
-
-    constructor(
-        port: any = RandomUtil.randomInteger(10000, 60000),
-        listen = '',
-        protocol = Protocols.VLESS,
-        settings = null,
-        streamSettings = new StreamSettings(),
-        tag = '',
-        sniffing = new Sniffing(),
-        clientStats = '',
-    ) {
-        super();
-        this.port = port;
-        this.listen = listen;
-        this._protocol = protocol;
-        this.settings = ObjectUtil.isEmpty(settings) ? Inbound.Settings.getSettings(protocol) : settings;
-        this.stream = streamSettings;
-        this.tag = tag;
-        this.sniffing = sniffing;
-        this.clientStats = clientStats;
-    }
-    getClientStats() {
-        return this.clientStats;
-    }
-
-    // Looks for a "host"-named entry in xhttp.headers and returns its value,
-    // or '' if not found. Used as a fallback when xhttp.host is empty so the
-    // share URL still carries a usable Host hint.
-    static xhttpHostFallback(xhttp: any): string {
-        if (!xhttp || !Array.isArray(xhttp.headers)) return '';
-        for (const h of xhttp.headers) {
-            if (h && typeof h.name === 'string' && h.name.toLowerCase() === 'host') {
-                return h.value || '';
-            }
-        }
-        return '';
-    }
-
-    // Build the JSON blob that goes into the URL's `extra` param (or, for
-    // VMess, into the base64-encoded link object). Carries ONLY the
-    // bidirectional fields from xray-core's SplitHTTPConfig — i.e. the
-    // ones the server enforces and the client must match. Strictly
-    // one-sided fields are excluded:
-    //
-    //   - server-only (noSSEHeader, scMaxBufferedPosts,
-    //     scStreamUpServerSecs, serverMaxHeaderBytes) — client wouldn't
-    //     read them, so emitting them just bloats the URL.
-    //   - client-only values are included only when present on the inbound
-    //     object. Imported/API-created configs can carry them there, and
-    //     the share link is the only place clients can receive them.
-    //
-    // Truthy-only guards keep default inbounds emitting the same compact
-    // URL they did before this helper grew.
-    static buildXhttpExtra(xhttp: any): any {
-        if (!xhttp) return null;
-        const extra: any = {};
-
-        if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
-            extra.xPaddingBytes = xhttp.xPaddingBytes;
-        }
-        if (xhttp.xPaddingObfsMode === true) {
-            extra.xPaddingObfsMode = true;
-            ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach((k: string) => {
-                if (typeof xhttp[k] === 'string' && xhttp[k].length > 0) {
-                    extra[k] = xhttp[k];
-                }
-            });
-        }
-
-        const stringFields = [
-            "uplinkHTTPMethod",
-            "sessionPlacement", "sessionKey",
-            "seqPlacement", "seqKey",
-            "uplinkDataPlacement", "uplinkDataKey",
-            "scMaxEachPostBytes", "scMinPostsIntervalMs",
-        ];
-        for (const k of stringFields) {
-            const v = xhttp[k];
-            if (typeof v === 'string' && v.length > 0) extra[k] = v;
-        }
-
-        const uplinkChunkSize = xhttp.uplinkChunkSize;
-        if ((typeof uplinkChunkSize === 'number' && uplinkChunkSize !== 0) ||
-            (typeof uplinkChunkSize === 'string' && uplinkChunkSize.length > 0)) {
-            extra.uplinkChunkSize = uplinkChunkSize;
-        }
-
-        if (xhttp.noGRPCHeader === true) {
-            extra.noGRPCHeader = true;
-        }
-
-        for (const k of ["xmux", "downloadSettings"]) {
-            const v = xhttp[k];
-            if (v && typeof v === 'object' && Object.keys(v).length > 0) {
-                extra[k] = v;
-            }
-        }
-
-        // Headers — emitted as the {name: value} map upstream's struct
-        // expects. The server runtime ignores this field, but the client
-        // (consuming the share link) honors it.
-        if (Array.isArray(xhttp.headers) && xhttp.headers.length > 0) {
-            const headersMap: any = {};
-            for (const h of xhttp.headers) {
-                if (h && h.name && h.name.toLowerCase() !== 'host') {
-                    headersMap[h.name] = h.value || '';
-                }
-            }
-            if (Object.keys(headersMap).length > 0) extra.headers = headersMap;
-        }
-
-        return Object.keys(extra).length > 0 ? extra : null;
-    }
-
-    // Inject the inbound-side xhttp config into URL query params for
-    // vless/trojan/ss links. Sets path/host/mode at top level (xray's
-    // Build() always lets these win over `extra`) and packs the
-    // bidirectional fields into a JSON `extra` param. Also writes the
-    // flat `x_padding_bytes` param sing-box-family clients understand.
-    //
-    // Without this, the admin's custom xPaddingBytes / sessionKey / etc.
-    // never reach the client and handshakes are silently rejected with
-    // `invalid padding (...) length: 0`.
-    static applyXhttpExtraToParams(xhttp: any, params: any): void {
-        if (!xhttp) return;
-        params.set("path", xhttp.path);
-        const host = xhttp.host?.length > 0 ? xhttp.host : Inbound.xhttpHostFallback(xhttp);
-        params.set("host", host);
-        params.set("mode", xhttp.mode);
-
-        // Flat fallback for sing-box-family clients that don't read `extra`.
-        if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
-            params.set("x_padding_bytes", xhttp.xPaddingBytes);
-        }
-
-        const extra = Inbound.buildXhttpExtra(xhttp);
-        if (extra) params.set("extra", JSON.stringify(extra));
-    }
-
-    // VMess variant: VMess links are a base64-encoded JSON object, so we
-    // copy the same bidirectional fields directly into the JSON instead
-    // of building a query string. (The base VMess link generator already
-    // sets net/type/path/host, so we only contribute the SplitHTTPConfig
-    // extra side here.)
-    static applyXhttpExtraToObj(xhttp: any, obj: any): void {
-        if (!xhttp || !obj) return;
-        if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
-            obj.x_padding_bytes = xhttp.xPaddingBytes;
-        }
-        const extra = Inbound.buildXhttpExtra(xhttp);
-        if (!extra) return;
-        for (const [k, v] of Object.entries(extra)) {
-            obj[k] = v;
-        }
-    }
-
-    static externalProxyAlpn(value: any): any {
-        if (Array.isArray(value)) return value.filter(Boolean).join(',');
-        return typeof value === 'string' ? value : '';
-    }
-
-    static applyExternalProxyTLSParams(externalProxy: any, params: any, security: any): void {
-        if (!externalProxy || security !== 'tls') return;
-        const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest;
-        if (sni?.length > 0) params.set("sni", sni);
-        if (externalProxy.fingerprint?.length > 0) params.set("fp", externalProxy.fingerprint);
-        const alpn = Inbound.externalProxyAlpn(externalProxy.alpn);
-        if (alpn.length > 0) params.set("alpn", alpn);
-    }
-
-    static applyExternalProxyTLSObj(externalProxy: any, obj: any, security: any): void {
-        if (!externalProxy || !obj || security !== 'tls') return;
-        const sni = externalProxy.sni?.length > 0 ? externalProxy.sni : externalProxy.dest;
-        if (sni?.length > 0) obj.sni = sni;
-        if (externalProxy.fingerprint?.length > 0) obj.fp = externalProxy.fingerprint;
-        const alpn = Inbound.externalProxyAlpn(externalProxy.alpn);
-        if (alpn.length > 0) obj.alpn = alpn;
-    }
-
-    static hasShareableFinalMaskValue(value: any): boolean {
-        if (value == null) {
-            return false;
-        }
-        if (Array.isArray(value)) {
-            return value.some((item: any) => Inbound.hasShareableFinalMaskValue(item));
-        }
-        if (typeof value === 'object') {
-            return Object.values(value).some((item: any) => Inbound.hasShareableFinalMaskValue(item));
-        }
-        if (typeof value === 'string') {
-            return value.length > 0;
-        }
-        return true;
-    }
-
-    static serializeFinalMask(finalmask: any): any {
-        if (!finalmask) {
-            return '';
-        }
-        const value = typeof finalmask.toJson === 'function' ? finalmask.toJson() : finalmask;
-        return Inbound.hasShareableFinalMaskValue(value) ? JSON.stringify(value) : '';
-    }
-
-    // Export finalmask with the same compact JSON payload shape that
-    // v2rayN-compatible share links use: fm=<json>.
-    static applyFinalMaskToParams(finalmask: any, params: any): void {
-        if (!params) return;
-        const payload = Inbound.serializeFinalMask(finalmask);
-        if (payload.length > 0) {
-            params.set("fm", payload);
-        }
-    }
-
-    // VMess links are a base64 JSON object, so keep the same fm payload
-    // under a flat property instead of a URL query string.
-    static applyFinalMaskToObj(finalmask: any, obj: any): void {
-        if (!obj) return;
-        const payload = Inbound.serializeFinalMask(finalmask);
-        if (payload.length > 0) {
-            obj.fm = payload;
-        }
-    }
-
-    get clients() {
-        switch (this.protocol) {
-            case Protocols.VMESS: return this.settings.vmesses;
-            case Protocols.VLESS: return this.settings.vlesses;
-            case Protocols.TROJAN: return this.settings.trojans;
-            case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
-            case Protocols.HYSTERIA: return this.settings.hysterias;
-            default: return null;
-        }
-    }
-
-    get protocol() {
-        return this._protocol;
-    }
-
-    set protocol(protocol) {
-        this._protocol = protocol;
-        this.settings = Inbound.Settings.getSettings(protocol);
-        this.stream = new StreamSettings();
-        if (protocol === Protocols.TROJAN) {
-            this.tls = false;
-        }
-        if (protocol === Protocols.HYSTERIA) {
-            this.stream.network = 'hysteria';
-            this.stream.security = 'tls';
-            // Hysteria runs over QUIC and must not inherit TCP TLS ALPN defaults.
-            this.stream.tls.alpn = [ALPN_OPTION.H3];
-        }
-    }
-
-    get network() {
-        return this.stream.network;
-    }
-
-    set network(network) {
-        this.stream.network = network;
-    }
-
-    get isTcp() {
-        return this.network === "tcp";
-    }
-
-    get isWs() {
-        return this.network === "ws";
-    }
-
-    get isKcp() {
-        return this.network === "kcp";
-    }
-
-    get isGrpc() {
-        return this.network === "grpc";
-    }
-
-    get isHttpupgrade() {
-        return this.network === "httpupgrade";
-    }
-
-    get isXHTTP() {
-        return this.network === "xhttp";
-    }
-
-    // Shadowsocks
-    get method() {
-        switch (this.protocol) {
-            case Protocols.SHADOWSOCKS:
-                return this.settings.method;
-            default:
-                return "";
-        }
-    }
-    get isSSMultiUser() {
-        return this.method != SSMethods.BLAKE3_CHACHA20_POLY1305;
-    }
-    get isSS2022() {
-        return this.method.substring(0, 4) === "2022";
-    }
-
-    get serverName() {
-        if (this.stream.isTls) return this.stream.tls.sni;
-        if (this.stream.isReality) return this.stream.reality.serverNames;
-        return "";
-    }
-
-    getHeader(obj: any, name: any) {
-        for (const header of obj.headers) {
-            if (header.name.toLowerCase() === name.toLowerCase()) {
-                return header.value;
-            }
-        }
-        return "";
-    }
-
-    get host() {
-        if (this.isTcp) {
-            return this.getHeader(this.stream.tcp.request, 'host');
-        } else if (this.isWs) {
-            return this.stream.ws.host?.length > 0 ? this.stream.ws.host : this.getHeader(this.stream.ws, 'host');
-        } else if (this.isHttpupgrade) {
-            return this.stream.httpupgrade.host?.length > 0 ? this.stream.httpupgrade.host : this.getHeader(this.stream.httpupgrade, 'host');
-        } else if (this.isXHTTP) {
-            return this.stream.xhttp.host?.length > 0 ? this.stream.xhttp.host : this.getHeader(this.stream.xhttp, 'host');
-        }
-        return null;
-    }
-
-    get path() {
-        if (this.isTcp) {
-            return this.stream.tcp.request.path[0];
-        } else if (this.isWs) {
-            return this.stream.ws.path;
-        } else if (this.isHttpupgrade) {
-            return this.stream.httpupgrade.path;
-        } else if (this.isXHTTP) {
-            return this.stream.xhttp.path;
-        }
-        return null;
-    }
-
-    get serviceName() {
-        return this.stream.grpc.serviceName;
-    }
-
-    isExpiry(index: number) {
-        const exp = this.clients[index].expiryTime;
-        return exp > 0 ? exp < new Date().getTime() : false;
-    }
-
-    canEnableTls() {
-        if (this.protocol === Protocols.HYSTERIA) return true;
-        if (![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
-        return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.network);
-    }
-
-    //this is used for xtls-rprx-vision
-    canEnableTlsFlow() {
-        if (((this.stream.security === 'tls') || (this.stream.security === 'reality')) && (this.network === "tcp")) {
-            return this.protocol === Protocols.VLESS;
-        }
-        return false;
-    }
-
-    // Vision seed applies only when the XTLS Vision (TCP/TLS) flow is selected.
-    // Excludes the UDP variant per spec.
-    canEnableVisionSeed() {
-        if (!this.canEnableTlsFlow()) return false;
-        const clients = this.settings?.vlesses;
-        if (!Array.isArray(clients)) return false;
-        return clients.some((c: any) => c?.flow === TLS_FLOW_CONTROL.VISION);
-    }
-
-    canEnableReality() {
-        if (![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
-        return ["tcp", "http", "grpc", "xhttp"].includes(this.network);
-    }
-
-    canEnableStream() {
-        return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA].includes(this.protocol);
-    }
-
-    reset() {
-        this.port = RandomUtil.randomInteger(10000, 60000);
-        this.listen = '';
-        this.protocol = Protocols.VMESS;
-        this.settings = Inbound.Settings.getSettings(Protocols.VMESS);
-        this.stream = new StreamSettings();
-        this.tag = '';
-        this.sniffing = new Sniffing();
-    }
-
-    genVmessLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientId?: any, security?: any, externalProxy: any = null) {
-        if (this.protocol !== Protocols.VMESS) {
-            return '';
-        }
-        const tls = forceTls == 'same' ? this.stream.security : forceTls;
-        const obj: any = {
-            v: '2',
-            ps: remark,
-            add: address,
-            port: port,
-            id: clientId,
-            scy: security,
-            net: this.stream.network,
-            tls: tls,
-        };
-        const network = this.stream.network;
-        if (network === 'tcp') {
-            const tcp = this.stream.tcp;
-            obj.type = tcp.type;
-            if (tcp.type === 'http') {
-                const request = tcp.request;
-                obj.path = request.path.join(',');
-                const host = this.getHeader(request, 'host');
-                if (host) obj.host = host;
-            }
-        } else if (network === 'kcp') {
-            const kcp = this.stream.kcp;
-            obj.mtu = kcp.mtu;
-            obj.tti = kcp.tti;
-        } else if (network === 'ws') {
-            const ws = this.stream.ws;
-            obj.path = ws.path;
-            obj.host = ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host');
-        } else if (network === 'grpc') {
-            obj.path = this.stream.grpc.serviceName;
-            obj.authority = this.stream.grpc.authority;
-            if (this.stream.grpc.multiMode) {
-                obj.type = 'multi'
-            }
-        } else if (network === 'httpupgrade') {
-            const httpupgrade = this.stream.httpupgrade;
-            obj.path = httpupgrade.path;
-            obj.host = httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host');
-        } else if (network === 'xhttp') {
-            const xhttp = this.stream.xhttp;
-            obj.path = xhttp.path;
-            obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host');
-            obj.type = xhttp.mode;
-            Inbound.applyXhttpExtraToObj(xhttp, obj);
-        }
-
-        Inbound.applyFinalMaskToObj(this.stream.finalmask, obj);
-
-        if (tls === 'tls') {
-            if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
-                obj.sni = this.stream.tls.sni;
-            }
-            if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)) {
-                obj.fp = this.stream.tls.settings.fingerprint;
-            }
-            if (this.stream.tls.alpn.length > 0) {
-                obj.alpn = this.stream.tls.alpn.join(',');
-            }
-        }
-        Inbound.applyExternalProxyTLSObj(externalProxy, obj, tls);
-
-        return 'vmess://' + Base64.encode(JSON.stringify(obj, null, 2));
-    }
-
-    genVLESSLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientId?: any, flow?: any, externalProxy: any = null) {
-        const uuid = clientId;
-        const type = this.stream.network;
-        const security = forceTls == 'same' ? this.stream.security : forceTls;
-        const params = new Map();
-        params.set("type", this.stream.network);
-        params.set("encryption", this.settings.encryption);
-        switch (type) {
-            case "tcp": {
-                const tcp = this.stream.tcp;
-                if (tcp.type === 'http') {
-                    const request = tcp.request;
-                    params.set("path", request.path.join(','));
-                    const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host');
-                    if (index >= 0) {
-                        const host = request.headers[index].value;
-                        params.set("host", host);
-                    }
-                    params.set("headerType", 'http');
-                }
-                break;
-            }
-            case "kcp": {
-                const kcp = this.stream.kcp;
-                params.set("mtu", kcp.mtu);
-                params.set("tti", kcp.tti);
-                break;
-            }
-            case "ws": {
-                const ws = this.stream.ws;
-                params.set("path", ws.path);
-                params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));
-                break;
-            }
-            case "grpc": {
-                const grpc = this.stream.grpc;
-                params.set("serviceName", grpc.serviceName);
-                params.set("authority", grpc.authority);
-                if (grpc.multiMode) {
-                    params.set("mode", "multi");
-                }
-                break;
-            }
-            case "httpupgrade": {
-                const httpupgrade = this.stream.httpupgrade;
-                params.set("path", httpupgrade.path);
-                params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
-                break;
-            }
-            case "xhttp":
-                Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
-                break;
-        }
-
-        Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
-
-        if (security === 'tls') {
-            params.set("security", "tls");
-            if (this.stream.isTls) {
-                params.set("fp", this.stream.tls.settings.fingerprint);
-                params.set("alpn", this.stream.tls.alpn);
-                if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
-                    params.set("sni", this.stream.tls.sni);
-                }
-                if (this.stream.tls.settings.echConfigList?.length > 0) {
-                    params.set("ech", this.stream.tls.settings.echConfigList);
-                }
-                if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
-                    params.set("flow", flow);
-                }
-            }
-            Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
-        }
-
-        else if (security === 'reality') {
-            params.set("security", "reality");
-            params.set("pbk", this.stream.reality.settings.publicKey);
-            params.set("fp", this.stream.reality.settings.fingerprint);
-            if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
-                params.set("sni", this.stream.reality.serverNames.split(",")[0]);
-            }
-            if (this.stream.reality.shortIds.length > 0) {
-                params.set("sid", this.stream.reality.shortIds.split(",")[0]);
-            }
-            if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
-                params.set("spx", this.stream.reality.settings.spiderX);
-            }
-            if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
-                params.set("pqv", this.stream.reality.settings.mldsa65Verify);
-            }
-            if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {
-                params.set("flow", flow);
-            }
-        }
-
-        else {
-            params.set("security", "none");
-        }
-
-        const link = `vless://${uuid}@${address}:${port}`;
-        const url = new URL(link);
-        for (const [key, value] of params) {
-            url.searchParams.set(key, value)
-        }
-        url.hash = encodeURIComponent(remark);
-        return url.toString();
-    }
-
-    genSSLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientPassword?: any, externalProxy: any = null) {
-        const settings = this.settings;
-        const type = this.stream.network;
-        const security = forceTls == 'same' ? this.stream.security : forceTls;
-        const params = new Map();
-        params.set("type", this.stream.network);
-        switch (type) {
-            case "tcp": {
-                const tcp = this.stream.tcp;
-                if (tcp.type === 'http') {
-                    const request = tcp.request;
-                    params.set("path", request.path.join(','));
-                    const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host');
-                    if (index >= 0) {
-                        const host = request.headers[index].value;
-                        params.set("host", host);
-                    }
-                    params.set("headerType", 'http');
-                }
-                break;
-            }
-            case "kcp": {
-                const kcp = this.stream.kcp;
-                params.set("mtu", kcp.mtu);
-                params.set("tti", kcp.tti);
-                break;
-            }
-            case "ws": {
-                const ws = this.stream.ws;
-                params.set("path", ws.path);
-                params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));
-                break;
-            }
-            case "grpc": {
-                const grpc = this.stream.grpc;
-                params.set("serviceName", grpc.serviceName);
-                params.set("authority", grpc.authority);
-                if (grpc.multiMode) {
-                    params.set("mode", "multi");
-                }
-                break;
-            }
-            case "httpupgrade": {
-                const httpupgrade = this.stream.httpupgrade;
-                params.set("path", httpupgrade.path);
-                params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
-                break;
-            }
-            case "xhttp":
-                Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
-                break;
-        }
-
-        Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
-
-        if (security === 'tls') {
-            params.set("security", "tls");
-            if (this.stream.isTls) {
-                params.set("fp", this.stream.tls.settings.fingerprint);
-                params.set("alpn", this.stream.tls.alpn);
-                if (this.stream.tls.settings.echConfigList?.length > 0) {
-                    params.set("ech", this.stream.tls.settings.echConfigList);
-                }
-                if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
-                    params.set("sni", this.stream.tls.sni);
-                }
-            }
-            Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
-        }
-
-
-        const password: string[] = [];
-        if (this.isSS2022) password.push(settings.password);
-        if (this.isSSMultiUser) password.push(clientPassword);
-
-        const link = `ss://${Base64.encode(`${settings.method}:${password.join(':')}`, true)}@${address}:${port}`;
-        const url = new URL(link);
-        for (const [key, value] of params) {
-            url.searchParams.set(key, value)
-        }
-        url.hash = encodeURIComponent(remark);
-        return url.toString();
-    }
-
-    genTrojanLink(address: any = '', port: any = this.port, forceTls?: any, remark: any = '', clientPassword?: any, externalProxy: any = null) {
-        const security = forceTls == 'same' ? this.stream.security : forceTls;
-        const type = this.stream.network;
-        const params = new Map();
-        params.set("type", this.stream.network);
-        switch (type) {
-            case "tcp": {
-                const tcp = this.stream.tcp;
-                if (tcp.type === 'http') {
-                    const request = tcp.request;
-                    params.set("path", request.path.join(','));
-                    const index = request.headers.findIndex((header: any) => header.name.toLowerCase() === 'host');
-                    if (index >= 0) {
-                        const host = request.headers[index].value;
-                        params.set("host", host);
-                    }
-                    params.set("headerType", 'http');
-                }
-                break;
-            }
-            case "kcp": {
-                const kcp = this.stream.kcp;
-                params.set("mtu", kcp.mtu);
-                params.set("tti", kcp.tti);
-                break;
-            }
-            case "ws": {
-                const ws = this.stream.ws;
-                params.set("path", ws.path);
-                params.set("host", ws.host?.length > 0 ? ws.host : this.getHeader(ws, 'host'));
-                break;
-            }
-            case "grpc": {
-                const grpc = this.stream.grpc;
-                params.set("serviceName", grpc.serviceName);
-                params.set("authority", grpc.authority);
-                if (grpc.multiMode) {
-                    params.set("mode", "multi");
-                }
-                break;
-            }
-            case "httpupgrade": {
-                const httpupgrade = this.stream.httpupgrade;
-                params.set("path", httpupgrade.path);
-                params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
-                break;
-            }
-            case "xhttp":
-                Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
-                break;
-        }
-
-        Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
-
-        if (security === 'tls') {
-            params.set("security", "tls");
-            if (this.stream.isTls) {
-                params.set("fp", this.stream.tls.settings.fingerprint);
-                params.set("alpn", this.stream.tls.alpn);
-                if (this.stream.tls.settings.echConfigList?.length > 0) {
-                    params.set("ech", this.stream.tls.settings.echConfigList);
-                }
-                if (!ObjectUtil.isEmpty(this.stream.tls.sni)) {
-                    params.set("sni", this.stream.tls.sni);
-                }
-            }
-            Inbound.applyExternalProxyTLSParams(externalProxy, params, security);
-        }
-
-        else if (security === 'reality') {
-            params.set("security", "reality");
-            params.set("pbk", this.stream.reality.settings.publicKey);
-            params.set("fp", this.stream.reality.settings.fingerprint);
-            if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
-                params.set("sni", this.stream.reality.serverNames.split(",")[0]);
-            }
-            if (this.stream.reality.shortIds.length > 0) {
-                params.set("sid", this.stream.reality.shortIds.split(",")[0]);
-            }
-            if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
-                params.set("spx", this.stream.reality.settings.spiderX);
-            }
-            if (!ObjectUtil.isEmpty(this.stream.reality.settings.mldsa65Verify)) {
-                params.set("pqv", this.stream.reality.settings.mldsa65Verify);
-            }
-        }
-
-        else {
-            params.set("security", "none");
-        }
-
-        const link = `trojan://${clientPassword}@${address}:${port}`;
-        const url = new URL(link);
-        for (const [key, value] of params) {
-            url.searchParams.set(key, value)
-        }
-        url.hash = encodeURIComponent(remark);
-        return url.toString();
-    }
-
-    genHysteriaLink(address: any = '', port: any = this.port, remark: any = '', clientAuth?: any) {
-        const protocol = this.settings.version == 2 ? "hysteria2" : "hysteria";
-        const link = `${protocol}://${clientAuth}@${address}:${port}`;
-
-        const params = new Map();
-        params.set("security", "tls");
-        if (this.stream.tls.settings.fingerprint?.length > 0) params.set("fp", this.stream.tls.settings.fingerprint);
-        if (this.stream.tls.alpn?.length > 0) params.set("alpn", this.stream.tls.alpn);
-        if (this.stream.tls.settings.allowInsecure) params.set("insecure", "1");
-        if (this.stream.tls.settings.echConfigList?.length > 0) params.set("ech", this.stream.tls.settings.echConfigList);
-        if (this.stream.tls.sni?.length > 0) params.set("sni", this.stream.tls.sni);
-
-        const udpMasks = this.stream?.finalmask?.udp;
-        if (Array.isArray(udpMasks)) {
-            const salamanderMask = udpMasks.find((mask: any) => mask?.type === 'salamander');
-            const obfsPassword = salamanderMask?.settings?.password;
-            if (typeof obfsPassword === 'string' && obfsPassword.length > 0) {
-                params.set("obfs", "salamander");
-                params.set("obfs-password", obfsPassword);
-            }
-        }
-
-        Inbound.applyFinalMaskToParams(this.stream.finalmask, params);
-
-        const url = new URL(link);
-        for (const [key, value] of params) {
-            url.searchParams.set(key, value);
-        }
-        url.hash = encodeURIComponent(remark);
-        return url.toString();
-    }
-
-    getWireguardTxt(address: any, port: any, remark: any, peerId: any) {
-        let txt = `[Interface]\n`
-        txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n`
-        txt += `Address = ${this.settings.peers[peerId].allowedIPs[0]}\n`
-        txt += `DNS = 1.1.1.1, 1.0.0.1\n`
-        if (this.settings.mtu) {
-            txt += `MTU = ${this.settings.mtu}\n`
-        }
-        txt += `\n# ${remark}\n`
-        txt += `[Peer]\n`
-        txt += `PublicKey = ${this.settings.pubKey}\n`
-        txt += `AllowedIPs = 0.0.0.0/0, ::/0\n`
-        txt += `Endpoint = ${address}:${port}`
-        if (this.settings.peers[peerId].psk) {
-            txt += `\nPresharedKey = ${this.settings.peers[peerId].psk}`
-        }
-        if (this.settings.peers[peerId].keepAlive) {
-            txt += `\nPersistentKeepalive = ${this.settings.peers[peerId].keepAlive}\n`
-        }
-        return txt;
-    }
-
-    getWireguardLink(address: any, port: any, remark: any, peerId: any) {
-        const peer = this.settings?.peers?.[peerId];
-        if (!peer) return '';
-
-        const link = `wireguard://${address}:${port}`;
-        const url = new URL(link);
-        url.username = peer.privateKey || '';
-
-        if (this.settings?.pubKey) {
-            url.searchParams.set("publickey", this.settings.pubKey);
-        }
-        if (Array.isArray(peer.allowedIPs) && peer.allowedIPs.length > 0 && peer.allowedIPs[0]) {
-            url.searchParams.set("address", peer.allowedIPs[0]);
-        }
-        if (this.settings?.mtu) {
-            url.searchParams.set("mtu", this.settings.mtu);
-        }
-
-        url.hash = encodeURIComponent(remark);
-        return url.toString();
-    }
-
-    // resolveAddr picks the host that goes into share/sub links. Order:
-    //   1. hostOverride (caller supplies node address for node-managed inbounds)
-    //   2. inbound's bind listen (when explicit, not 0.0.0.0)
-    //   3. browser's location.hostname (single-panel default)
-    // Centralised so genAllLinks/genInboundLinks/genWireguard*
-    // all share the same chain — pre-Phase 3 we had four duplicated lines.
-    _resolveAddr(hostOverride = '') {
-        if (hostOverride) return hostOverride;
-        if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") return this.listen;
-        return location.hostname;
-    }
-
-    genWireguardLinks(remark = '', remarkModel = '-ieo', hostOverride = '') {
-        const addr = this._resolveAddr(hostOverride);
-        const separationChar = remarkModel.charAt(0);
-        const links: any[] = [];
-        this.settings.peers.forEach((_p: any, index: number) => {
-            links.push(this.getWireguardLink(addr, this.port, remark + separationChar + (index + 1), index));
-        });
-        return links.join('\r\n');
-    }
-
-    genWireguardConfigs(remark = '', remarkModel = '-ieo', hostOverride = '') {
-        const addr = this._resolveAddr(hostOverride);
-        const separationChar = remarkModel.charAt(0);
-        const links: any[] = [];
-        this.settings.peers.forEach((_p: any, index: number) => {
-            links.push(this.getWireguardTxt(addr, this.port, remark + separationChar + (index + 1), index));
-        });
-        return links.join('\r\n');
-    }
-
-    genLink(address: any = '', port: any = this.port, forceTls: any = 'same', remark: any = '', client?: any, externalProxy: any = null) {
-        switch (this.protocol) {
-            case Protocols.VMESS:
-                return this.genVmessLink(address, port, forceTls, remark, client.id, client.security, externalProxy);
-            case Protocols.VLESS:
-                return this.genVLESSLink(address, port, forceTls, remark, client.id, client.flow, externalProxy);
-            case Protocols.SHADOWSOCKS:
-                return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '', externalProxy);
-            case Protocols.TROJAN:
-                return this.genTrojanLink(address, port, forceTls, remark, client.password, externalProxy);
-            case Protocols.HYSTERIA:
-                return this.genHysteriaLink(address, port, remark, client.auth.length > 0 ? client.auth : this.stream.hysteria.auth);
-            default: return '';
-        }
-    }
-
-    genAllLinks(remark: any = '', remarkModel: any = '-ieo', client?: any, hostOverride: any = '') {
-        const result: any[] = [];
-        const email = client ? client.email : '';
-        const addr = this._resolveAddr(hostOverride);
-        const port = this.port;
-        const separationChar = remarkModel.charAt(0);
-        const orderChars = remarkModel.slice(1);
-        const orders: any = {
-            'i': remark,
-            'e': email,
-            'o': '',
-        };
-        if (ObjectUtil.isArrEmpty(this.stream.externalProxy)) {
-            const r = orderChars.split('').map((char: string) => orders[char]).filter((x: any) => x.length > 0).join(separationChar);
-            result.push({
-                remark: r,
-                link: this.genLink(addr, port, 'same', r, client)
-            });
-        } else {
-            this.stream.externalProxy.forEach((ep: any) => {
-                orders['o'] = ep.remark;
-                const r = orderChars.split('').map((char: string) => orders[char]).filter((x: any) => x.length > 0).join(separationChar);
-                result.push({
-                    remark: r,
-                    link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client, ep)
-                });
-            });
-        }
-        return result;
-    }
-
-    genInboundLinks(remark = '', remarkModel = '-ieo', hostOverride = '') {
-        const addr = this._resolveAddr(hostOverride);
-        if (this.clients) {
-            const links: any[] = [];
-            this.clients.forEach((client: any) => {
-                this.genAllLinks(remark, remarkModel, client, hostOverride).forEach((l: any) => {
-                    links.push(l.link);
-                })
-            });
-            return links.join('\r\n');
-        } else {
-            if (this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(addr, this.port, 'same', remark);
-            if (this.protocol == Protocols.WIREGUARD) {
-                return this.genWireguardConfigs(remark, remarkModel, hostOverride);
-            }
-            return '';
-        }
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound(
-            json.port,
-            json.listen,
-            json.protocol,
-            Inbound.Settings.fromJson(json.protocol, json.settings),
-            StreamSettings.fromJson(json.streamSettings),
-            json.tag,
-            Sniffing.fromJson(json.sniffing),
-            json.clientStats
-        )
-    }
-
-    toJson() {
-        // Only these protocols use streamSettings
-        const streamProtocols = [Protocols.VLESS, Protocols.VMESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA];
-
-        const result: any = {
-            port: this.port,
-            listen: this.listen,
-            protocol: this.protocol,
-            settings: this.settings instanceof XrayCommonClass ? this.settings.toJson() : this.settings,
-            tag: this.tag,
-            sniffing: this.sniffing.toJson(),
-            clientStats: this.clientStats
-        };
-
-        // Only add streamSettings if protocol supports it
-        if (streamProtocols.includes(this.protocol)) {
-            result.streamSettings = this.stream.toJson();
-        }
-
-        return result;
-    }
-}
-
-Inbound.Settings = class extends XrayCommonClass {
-    constructor(protocol: any) {
-        super();
-        this.protocol = protocol;
-    }
-
-    static getSettings(protocol: any): any {
-        switch (protocol) {
-            case Protocols.VMESS: return new Inbound.VmessSettings(protocol);
-            case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
-            case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
-            case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
-            case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
-            case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
-            case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
-            case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
-            case Protocols.TUN: return new Inbound.TunSettings(protocol);
-            case Protocols.HYSTERIA: return new Inbound.HysteriaSettings(protocol);
-            default: return null;
-        }
-    }
-
-    static fromJson(protocol: any, json: any): any {
-        switch (protocol) {
-            case Protocols.VMESS: return Inbound.VmessSettings.fromJson(json);
-            case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
-            case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
-            case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
-            case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
-            case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
-            case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
-            case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
-            case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
-            case Protocols.HYSTERIA: return Inbound.HysteriaSettings.fromJson(json);
-            default: return null;
-        }
-    }
-
-    toJson() {
-        return {};
-    }
-};
-
-/** Shared user-quota fields and UI helpers for multi-user protocol clients. */
-Inbound.ClientBase = class extends XrayCommonClass {
-    constructor(
-        email: any = RandomUtil.randomLowerAndNum(8),
-        limitIp: any = 0,
-        totalGB: any = 0,
-        expiryTime: any = 0,
-        enable: any = true,
-        tgId: any = '',
-        subId: any = RandomUtil.randomLowerAndNum(16),
-        comment: any = '',
-        reset: any = 0,
-        created_at: any = undefined,
-        updated_at: any = undefined,
-    ) {
-        super();
-        this.email = email;
-        this.limitIp = limitIp;
-        this.totalGB = totalGB;
-        this.expiryTime = expiryTime;
-        this.enable = enable;
-        this.tgId = tgId;
-        this.subId = subId;
-        this.comment = comment;
-        this.reset = reset;
-        this.created_at = created_at;
-        this.updated_at = updated_at;
-    }
-
-    static commonArgsFromJson(json: any = {}) {
-        return [
-            json.email,
-            json.limitIp,
-            json.totalGB,
-            json.expiryTime,
-            json.enable,
-            json.tgId,
-            json.subId,
-            json.comment,
-            json.reset,
-            json.created_at,
-            json.updated_at,
-        ];
-    }
-
-    _clientBaseToJson() {
-        return {
-            email: this.email,
-            limitIp: this.limitIp,
-            totalGB: this.totalGB,
-            expiryTime: this.expiryTime,
-            enable: this.enable,
-            tgId: this.tgId,
-            subId: this.subId,
-            comment: this.comment,
-            reset: this.reset,
-            created_at: this.created_at,
-            updated_at: this.updated_at,
-        };
-    }
-
-    get _expiryTime() {
-        if (this.expiryTime === 0 || this.expiryTime === '') {
-            return null;
-        }
-        if (this.expiryTime < 0) {
-            return this.expiryTime / -86400000;
-        }
-        return dayjs(this.expiryTime);
-    }
-
-    set _expiryTime(t: any) {
-        if (t == null || t === '') {
-            this.expiryTime = 0;
-        } else {
-            this.expiryTime = t.valueOf();
-        }
-    }
-
-    get _totalGB() {
-        return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
-    }
-
-    set _totalGB(gb) {
-        this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
-    }
-};
-
-Inbound.VmessSettings = class extends Inbound.Settings {
-    constructor(protocol: any,
-        vmesses: any[] = []) {
-        super(protocol);
-        this.vmesses = vmesses;
-    }
-
-    indexOfVmessById(id: any) {
-        return this.vmesses.findIndex((VMESS: any) => VMESS.id === id);
-    }
-
-    addVmess(VMESS: any) {
-        if (this.indexOfVmessById(VMESS.id) >= 0) {
-            return false;
-        }
-        this.vmesses.push(VMESS);
-    }
-
-    delVmess(VMESS: any) {
-        const i = this.indexOfVmessById(VMESS.id);
-        if (i >= 0) {
-            this.vmesses.splice(i, 1);
-        }
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.VmessSettings(
-            Protocols.VMESS,
-            (json.clients || []).map((client: any) => Inbound.VmessSettings.VMESS.fromJson(client)),
-        );
-    }
-
-    toJson() {
-        return {
-            clients: Inbound.VmessSettings.toJsonArray(this.vmesses),
-        };
-    }
-};
-
-Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase {
-    constructor(
-        id: any = RandomUtil.randomUUID(),
-        security: any = USERS_SECURITY.AUTO,
-        email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
-    ) {
-        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
-        this.id = id;
-        this.security = security;
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.VmessSettings.VMESS(
-            json.id,
-            json.security,
-            ...Inbound.ClientBase.commonArgsFromJson(json),
-        );
-    }
-
-    toJson() {
-        return {
-            id: this.id,
-            security: this.security,
-            ...this._clientBaseToJson(),
-        };
-    }
-};
-
-Inbound.VLESSSettings = class extends Inbound.Settings {
-    constructor(
-        protocol: any,
-        vlesses: any[] = [],
-        decryption: any = "none",
-        encryption: any = "none",
-        fallbacks: any[] = [],
-        testseed: any[] = [],
-    ) {
-        super(protocol);
-        this.vlesses = vlesses;
-        this.decryption = decryption;
-        this.encryption = encryption;
-        this.fallbacks = fallbacks;
-        this.testseed = testseed;
-    }
-
-    addFallback() {
-        this.fallbacks.push(new Inbound.VLESSSettings.Fallback());
-    }
-
-    delFallback(index: number) {
-        this.fallbacks.splice(index, 1);
-    }
-
-    // Empty array means "use server defaults" (won't be sent).
-    // Anything else must be exactly 4 positive integers.
-    static isValidTestseed(arr: any): boolean {
-        if (!Array.isArray(arr) || arr.length === 0) return true;
-        if (arr.length !== 4) return false;
-        return arr.every((v: any) => Number.isInteger(v) && v > 0);
-    }
-
-    static fromJson(json: any = {}) {
-        // Preserve a saved testseed only if it's a valid 4-positive-int array; otherwise leave empty
-        // so toJson omits it and the form falls back to placeholder defaults.
-        const saved = json.testseed;
-        const testseed = (Array.isArray(saved)
-            && saved.length === 4
-            && saved.every((v: any) => Number.isInteger(v) && v > 0))
-            ? saved
-            : [];
-
-        const obj = new Inbound.VLESSSettings(
-            Protocols.VLESS,
-            (json.clients || []).map((client: any) => Inbound.VLESSSettings.VLESS.fromJson(client)),
-            json.decryption,
-            json.encryption,
-            Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
-            testseed,
-        );
-        return obj;
-    }
-
-
-    toJson() {
-        const json: any = {
-            clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
-        };
-
-        if (this.decryption) {
-            json.decryption = this.decryption;
-        }
-
-        if (this.encryption) {
-            json.encryption = this.encryption;
-        }
-
-        if (this.fallbacks && this.fallbacks.length > 0) {
-            json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
-        }
-
-        // testseed is only meaningful for the exact xtls-rprx-vision flow, and only when
-        // the user supplied a complete 4-positive-int array. Otherwise omit and let the
-        // backend fall back to its safe defaults.
-        const hasVisionFlow = this.vlesses && this.vlesses.some((v: any) => v.flow === TLS_FLOW_CONTROL.VISION);
-        if (hasVisionFlow
-            && Array.isArray(this.testseed)
-            && this.testseed.length === 4
-            && this.testseed.every((v: any) => Number.isInteger(v) && v > 0)) {
-            json.testseed = this.testseed;
-        }
-
-        return json;
-    }
-};
-
-Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase {
-    constructor(
-        id: any = RandomUtil.randomUUID(),
-        flow: any = '',
-        reverseTag: any = '',
-        reverseSniffing: any = new Sniffing(),
-        email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
-    ) {
-        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
-        this.id = id;
-        this.flow = flow;
-        this.reverseTag = reverseTag;
-        this.reverseSniffing = reverseSniffing;
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.VLESSSettings.VLESS(
-            json.id,
-            json.flow,
-            json.reverse?.tag ?? '',
-            Sniffing.fromJson(json.reverse?.sniffing || {}),
-            ...Inbound.ClientBase.commonArgsFromJson(json),
-        );
-    }
-
-    toJson() {
-        const json: any = {
-            id: this.id,
-            flow: this.flow,
-            ...this._clientBaseToJson(),
-        };
-        if (this.reverseTag) {
-            json.reverse = {
-                tag: this.reverseTag,
-            };
-        }
-        return json;
-    }
-};
-
-Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
-    constructor(name = "", alpn = '', path = '', dest = '', xver = 0) {
-        super();
-        this.name = name;
-        this.alpn = alpn;
-        this.path = path;
-        this.dest = dest;
-        this.xver = xver;
-    }
-
-    toJson() {
-        return XrayCommonClass.fallbackToJson(this as unknown as FallbackEntry);
-    }
-
-    static fromJson(json: any = []) {
-        return (json || []).map((f: any) => new Inbound.VLESSSettings.Fallback(
-            f.name, f.alpn, f.path, f.dest, f.xver,
-        ));
-    }
-};
-
-Inbound.TrojanSettings = class extends Inbound.Settings {
-    constructor(protocol: any,
-        trojans: any[] = [],
-        fallbacks: any[] = [],) {
-        super(protocol);
-        this.trojans = trojans;
-        this.fallbacks = fallbacks;
-    }
-
-    addFallback() {
-        this.fallbacks.push(new Inbound.TrojanSettings.Fallback());
-    }
-
-    delFallback(index: number) {
-        this.fallbacks.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.TrojanSettings(
-            Protocols.TROJAN,
-            (json.clients || []).map((client: any) => Inbound.TrojanSettings.Trojan.fromJson(client)),
-            Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
-    }
-
-    toJson() {
-        const json: any = {
-            clients: Inbound.TrojanSettings.toJsonArray(this.trojans),
-        };
-        if (this.fallbacks && this.fallbacks.length > 0) {
-            json.fallbacks = Inbound.TrojanSettings.toJsonArray(this.fallbacks);
-        }
-        return json;
-    }
-};
-
-Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase {
-    constructor(
-        password = RandomUtil.randomSeq(10),
-        email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
-    ) {
-        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
-        this.password = password;
-    }
-
-    toJson() {
-        return {
-            password: this.password,
-            ...this._clientBaseToJson(),
-        };
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.TrojanSettings.Trojan(
-            json.password,
-            ...Inbound.ClientBase.commonArgsFromJson(json),
-        );
-    }
-};
-
-Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
-    constructor(name = "", alpn = '', path = '', dest = '', xver = 0) {
-        super();
-        this.name = name;
-        this.alpn = alpn;
-        this.path = path;
-        this.dest = dest;
-        this.xver = xver;
-    }
-
-    toJson() {
-        return XrayCommonClass.fallbackToJson(this as unknown as FallbackEntry);
-    }
-
-    static fromJson(json: any = []) {
-        return (json || []).map((f: any) => new Inbound.TrojanSettings.Fallback(
-            f.name, f.alpn, f.path, f.dest, f.xver,
-        ));
-    }
-};
-
-Inbound.ShadowsocksSettings = class extends Inbound.Settings {
-    constructor(protocol: any,
-        method: any = SSMethods.BLAKE3_AES_256_GCM,
-        password: any = RandomUtil.randomShadowsocksPassword(),
-        network: any = 'tcp',
-        shadowsockses: any[] = [],
-        ivCheck = false,
-    ) {
-        super(protocol);
-        this.method = method;
-        this.password = password;
-        this.network = network;
-        this.shadowsockses = shadowsockses;
-        this.ivCheck = ivCheck;
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.ShadowsocksSettings(
-            Protocols.SHADOWSOCKS,
-            json.method,
-            json.password,
-            json.network,
-            (json.clients || []).map((client: any) => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
-            json.ivCheck,
-        );
-    }
-
-    toJson() {
-        return {
-            method: this.method,
-            password: this.password,
-            network: this.network,
-            clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses),
-            ivCheck: this.ivCheck,
-        };
-    }
-};
-
-Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase {
-    constructor(
-        method = '',
-        password = RandomUtil.randomShadowsocksPassword(),
-        email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
-    ) {
-        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
-        this.method = method;
-        this.password = password;
-    }
-
-    toJson() {
-        return {
-            method: this.method,
-            password: this.password,
-            ...this._clientBaseToJson(),
-        };
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.ShadowsocksSettings.Shadowsocks(
-            json.method,
-            json.password,
-            ...Inbound.ClientBase.commonArgsFromJson(json),
-        );
-    }
-};
-
-Inbound.HysteriaSettings = class extends Inbound.Settings {
-    constructor(protocol: any, version: any = 2, hysterias: any[] = []) {
-        super(protocol);
-        this.version = version;
-        this.hysterias = hysterias;
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.HysteriaSettings(
-            Protocols.HYSTERIA,
-            json.version ?? 2,
-            (json.clients || []).map((client: any) => Inbound.HysteriaSettings.Hysteria.fromJson(client)),
-        );
-    }
-
-    toJson() {
-        return {
-            version: this.version,
-            clients: Inbound.HysteriaSettings.toJsonArray(this.hysterias),
-        };
-    }
-};
-
-Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase {
-    constructor(
-        auth = RandomUtil.randomSeq(10),
-        email?: any, limitIp?: any, totalGB?: any, expiryTime?: any, enable?: any, tgId?: any, subId?: any, comment?: any, reset?: any, created_at?: any, updated_at?: any,
-    ) {
-        super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
-        this.auth = auth;
-    }
-
-    toJson() {
-        return {
-            auth: this.auth,
-            ...this._clientBaseToJson(),
-        };
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.HysteriaSettings.Hysteria(
-            json.auth,
-            ...Inbound.ClientBase.commonArgsFromJson(json),
-        );
-    }
-};
-
-Inbound.TunnelSettings = class extends Inbound.Settings {
-    constructor(
-        protocol: any,
-        rewriteAddress?: any,
-        rewritePort?: any,
-        portMap: any[] = [],
-        allowedNetwork: any = 'tcp,udp',
-        followRedirect: any = false
-    ) {
-        super(protocol);
-        this.rewriteAddress = rewriteAddress;
-        this.rewritePort = rewritePort;
-        this.portMap = portMap;
-        this.allowedNetwork = allowedNetwork;
-        this.followRedirect = followRedirect;
-    }
-
-    addPortMap(port = '', target = '') {
-        this.portMap.push({ name: port, value: target });
-    }
-
-    removePortMap(index: number) {
-        this.portMap.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.TunnelSettings(
-            Protocols.TUNNEL,
-            json.rewriteAddress,
-            json.rewritePort,
-            XrayCommonClass.toHeaders(json.portMap),
-            json.allowedNetwork,
-            json.followRedirect,
-        );
-    }
-
-    toJson() {
-        return {
-            rewriteAddress: this.rewriteAddress,
-            rewritePort: this.rewritePort,
-            portMap: XrayCommonClass.toV2Headers(this.portMap, false),
-            allowedNetwork: this.allowedNetwork,
-            followRedirect: this.followRedirect,
-        };
-    }
-};
-
-Inbound.MixedSettings = class extends Inbound.Settings {
-    constructor(protocol: any, auth: any = 'password', accounts: any[] = [new Inbound.MixedSettings.SocksAccount()], udp: any = false, ip: any = '127.0.0.1') {
-        super(protocol);
-        this.auth = auth;
-        this.accounts = accounts;
-        this.udp = udp;
-        this.ip = ip;
-    }
-
-    addAccount(account: any) {
-        this.accounts.push(account);
-    }
-
-    delAccount(index: number) {
-        this.accounts.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        let accounts;
-        if (json.auth === 'password') {
-            accounts = json.accounts.map(
-                (account: any) => Inbound.MixedSettings.SocksAccount.fromJson(account)
-            )
-        }
-        return new Inbound.MixedSettings(
-            Protocols.MIXED,
-            json.auth,
-            accounts,
-            json.udp,
-            json.ip,
-        );
-    }
-
-    toJson() {
-        return {
-            auth: this.auth,
-            accounts: this.auth === 'password' ? this.accounts.map((account: any) => account.toJson()) : undefined,
-            udp: this.udp,
-            ip: this.ip,
-        };
-    }
-};
-Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
-    constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
-        super();
-        this.user = user;
-        this.pass = pass;
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
-    }
-};
-
-Inbound.HttpSettings = class extends Inbound.Settings {
-    constructor(
-        protocol: any,
-        accounts: any[] = [new Inbound.HttpSettings.HttpAccount()],
-        allowTransparent: any = false,
-    ) {
-        super(protocol);
-        this.accounts = accounts;
-        this.allowTransparent = allowTransparent;
-    }
-
-    addAccount(account: any) {
-        this.accounts.push(account);
-    }
-
-    delAccount(index: number) {
-        this.accounts.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.HttpSettings(
-            Protocols.HTTP,
-            json.accounts.map((account: any) => Inbound.HttpSettings.HttpAccount.fromJson(account)),
-            json.allowTransparent,
-        );
-    }
-
-    toJson() {
-        return {
-            accounts: Inbound.HttpSettings.toJsonArray(this.accounts),
-            allowTransparent: this.allowTransparent,
-        };
-    }
-};
-
-Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass {
-    constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
-        super();
-        this.user = user;
-        this.pass = pass;
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.HttpSettings.HttpAccount(json.user, json.pass);
-    }
-};
-
-Inbound.WireguardSettings = class extends XrayCommonClass {
-    constructor(
-        protocol?: any,
-        mtu: any = 1420,
-        secretKey: any = Wireguard.generateKeypair().privateKey,
-        peers: any[] = [new Inbound.WireguardSettings.Peer()],
-        noKernelTun: any = false
-    ) {
-        super();
-        this.protocol = protocol;
-        this.mtu = mtu;
-        this.secretKey = secretKey;
-        this.pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
-        this.peers = peers;
-        this.noKernelTun = noKernelTun;
-    }
-
-    addPeer() {
-        this.peers.push(new Inbound.WireguardSettings.Peer(null, null, '', ['10.0.0.' + (this.peers.length + 2)]));
-    }
-
-    delPeer(index: number) {
-        this.peers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.WireguardSettings(
-            Protocols.WIREGUARD,
-            json.mtu,
-            json.secretKey,
-            json.peers.map((peer: any) => Inbound.WireguardSettings.Peer.fromJson(peer)),
-            json.noKernelTun,
-        );
-    }
-
-    toJson() {
-        return {
-            mtu: this.mtu ?? undefined,
-            secretKey: this.secretKey,
-            peers: Inbound.WireguardSettings.Peer.toJsonArray(this.peers),
-            noKernelTun: this.noKernelTun,
-        };
-    }
-};
-
-Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
-    constructor(privateKey?: any, publicKey?: any, psk: any = '', allowedIPs: any[] = ['10.0.0.2/32'], keepAlive: any = 0) {
-        super();
-        this.privateKey = privateKey
-        this.publicKey = publicKey;
-        if (!this.publicKey) {
-            [this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair())
-        }
-        this.psk = psk;
-        allowedIPs.forEach((a: any, index: number) => {
-            if (a.length > 0 && !a.includes('/')) allowedIPs[index] += '/32';
-        })
-        this.allowedIPs = allowedIPs;
-        this.keepAlive = keepAlive;
-    }
-
-    static fromJson(json: any = {}) {
-        return new Inbound.WireguardSettings.Peer(
-            json.privateKey,
-            json.publicKey,
-            json.preSharedKey,
-            json.allowedIPs,
-            json.keepAlive
-        );
-    }
-
-    toJson() {
-        this.allowedIPs.forEach((a: any, index: number) => {
-            if (a.length > 0 && !a.includes('/')) this.allowedIPs[index] += '/32';
-        });
-        return {
-            privateKey: this.privateKey,
-            publicKey: this.publicKey,
-            preSharedKey: this.psk.length > 0 ? this.psk : undefined,
-            allowedIPs: this.allowedIPs,
-            keepAlive: this.keepAlive ?? undefined,
-        };
-    }
-};
-
-Inbound.TunSettings = class extends Inbound.Settings {
-    constructor(
-        protocol: any,
-        name: any = 'xray0',
-        mtu: any = 1500,
-        gateway: any[] = [],
-        dns: any[] = [],
-        userLevel: any = 0,
-        autoSystemRoutingTable: any[] = [],
-        autoOutboundsInterface = 'auto'
-    ) {
-        super(protocol);
-        this.name = name;
-        this.mtu = Number(mtu) || 1500;
-        this.gateway = Array.isArray(gateway) ? gateway : [];
-        this.dns = Array.isArray(dns) ? dns : [];
-        this.userLevel = userLevel;
-        this.autoSystemRoutingTable = Array.isArray(autoSystemRoutingTable) ? autoSystemRoutingTable : [];
-        this.autoOutboundsInterface = autoOutboundsInterface;
-    }
-
-    static fromJson(json: any = {}) {
-        const rawMtu = json.mtu ?? json.MTU;
-        const mtu = Array.isArray(rawMtu) ? rawMtu[0] : rawMtu;
-        return new Inbound.TunSettings(
-            Protocols.TUN,
-            json.name ?? 'xray0',
-            mtu ?? 1500,
-            json.gateway ?? json.Gateway ?? [],
-            json.dns ?? json.DNS ?? [],
-            json.userLevel ?? 0,
-            json.autoSystemRoutingTable ?? [],
-            Object.prototype.hasOwnProperty.call(json, 'autoOutboundsInterface') ? json.autoOutboundsInterface : 'auto'
-        );
-    }
-
-    toJson() {
-        return {
-            name: this.name || 'xray0',
-            mtu: Number(this.mtu) || 1500,
-            gateway: this.gateway,
-            dns: this.dns,
-            userLevel: this.userLevel || 0,
-            autoSystemRoutingTable: this.autoSystemRoutingTable,
-            autoOutboundsInterface: this.autoOutboundsInterface,
-        };
-    }
-};

+ 0 - 2405
frontend/src/models/outbound.ts

@@ -1,2405 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { ObjectUtil, Base64, Wireguard } from '@/utils';
-
-export const Protocols = {
-    Freedom: "freedom",
-    Blackhole: "blackhole",
-    DNS: "dns",
-    VMess: "vmess",
-    VLESS: "vless",
-    Trojan: "trojan",
-    Shadowsocks: "shadowsocks",
-    Wireguard: "wireguard",
-    Hysteria: "hysteria",
-    Socks: "socks",
-    HTTP: "http",
-    Loopback: "loopback",
-};
-
-export const SSMethods = {
-    AES_256_GCM: 'aes-256-gcm',
-    AES_128_GCM: 'aes-128-gcm',
-    CHACHA20_POLY1305: 'chacha20-poly1305',
-    CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
-    XCHACHA20_POLY1305: 'xchacha20-poly1305',
-    XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
-    BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
-    BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
-    BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
-};
-
-export const TLS_FLOW_CONTROL = {
-    VISION: "xtls-rprx-vision",
-    VISION_UDP443: "xtls-rprx-vision-udp443",
-};
-
-export const UTLS_FINGERPRINT = {
-    UTLS_CHROME: "chrome",
-    UTLS_FIREFOX: "firefox",
-    UTLS_SAFARI: "safari",
-    UTLS_IOS: "ios",
-    UTLS_android: "android",
-    UTLS_EDGE: "edge",
-    UTLS_360: "360",
-    UTLS_QQ: "qq",
-    UTLS_RANDOM: "random",
-    UTLS_RANDOMIZED: "randomized",
-    UTLS_RONDOMIZEDNOALPN: "randomizednoalpn",
-    UTLS_UNSAFE: "unsafe",
-};
-
-export const ALPN_OPTION = {
-    H3: "h3",
-    H2: "h2",
-    HTTP1: "http/1.1",
-};
-
-export const SNIFFING_OPTION = {
-    HTTP: "http",
-    TLS: "tls",
-    QUIC: "quic",
-    FAKEDNS: "fakedns"
-};
-
-export const OutboundDomainStrategies = [
-    "AsIs",
-    "UseIP",
-    "UseIPv4",
-    "UseIPv6",
-    "UseIPv6v4",
-    "UseIPv4v6",
-    "ForceIP",
-    "ForceIPv6v4",
-    "ForceIPv6",
-    "ForceIPv4v6",
-    "ForceIPv4"
-];
-
-export const WireguardDomainStrategy = [
-    "ForceIP",
-    "ForceIPv4",
-    "ForceIPv4v6",
-    "ForceIPv6",
-    "ForceIPv6v4"
-];
-
-export const USERS_SECURITY = {
-    AES_128_GCM: "aes-128-gcm",
-    CHACHA20_POLY1305: "chacha20-poly1305",
-    AUTO: "auto",
-    NONE: "none",
-    ZERO: "zero",
-};
-
-export const MODE_OPTION = {
-    AUTO: "auto",
-    PACKET_UP: "packet-up",
-    STREAM_UP: "stream-up",
-    STREAM_ONE: "stream-one",
-};
-
-export const Address_Port_Strategy = {
-    NONE: "none",
-    SrvPortOnly: "srvportonly",
-    SrvAddressOnly: "srvaddressonly",
-    SrvPortAndAddress: "srvportandaddress",
-    TxtPortOnly: "txtportonly",
-    TxtAddressOnly: "txtaddressonly",
-    TxtPortAndAddress: "txtportandaddress"
-};
-
-export const DNSRuleActions = ['direct', 'drop', 'reject', 'hijack'];
-
-export function normalizeDNSRuleField(value: any): string {
-    if (value === null || value === undefined) {
-        return '';
-    }
-    if (Array.isArray(value)) {
-        return value.map((item: any) => item.toString().trim()).filter((item: any) => item.length > 0).join(',');
-    }
-    return value.toString().trim();
-}
-
-export function normalizeDNSRuleAction(action: any): string {
-    action = ObjectUtil.isEmpty(action) ? 'direct' : action.toString().toLowerCase().trim();
-    return DNSRuleActions.includes(action) ? action : 'direct';
-}
-
-export function parseLegacyDNSBlockTypes(blockTypes: any): number[] {
-    if (blockTypes === null || blockTypes === undefined || blockTypes === '') {
-        return [];
-    }
-
-    if (Array.isArray(blockTypes)) {
-        return blockTypes
-            .map((item: any) => Number(item))
-            .filter((item: any) => Number.isInteger(item) && item >= 0 && item <= 65535);
-    }
-
-    if (typeof blockTypes === 'number') {
-        return Number.isInteger(blockTypes) && blockTypes >= 0 && blockTypes <= 65535 ? [blockTypes] : [];
-    }
-
-    return blockTypes
-        .toString()
-        .split(',')
-        .map((item: any) => item.trim())
-        .filter((item: any) => /^\d+$/.test(item))
-        .map((item: any) => Number(item))
-        .filter((item: any) => item >= 0 && item <= 65535);
-}
-
-export function buildLegacyDNSRules(nonIPQuery: any, blockTypes: any): any[] {
-    const mode = ['reject', 'drop', 'skip'].includes(nonIPQuery) ? nonIPQuery : 'reject';
-    const rules = [];
-    const parsedBlockTypes = parseLegacyDNSBlockTypes(blockTypes);
-
-    if (parsedBlockTypes.length > 0) {
-        rules.push(new Outbound.DNSRule(mode === 'reject' ? 'reject' : 'drop', parsedBlockTypes.join(',')));
-    }
-
-    rules.push(new Outbound.DNSRule('hijack', '1,28'));
-    rules.push(new Outbound.DNSRule(mode === 'skip' ? 'direct' : mode));
-
-    return rules;
-}
-
-export function getDNSRulesFromJson(json: any = {}): any[] {
-    if (Array.isArray(json.rules) && json.rules.length > 0) {
-        return json.rules.map((rule: any) => Outbound.DNSRule.fromJson(rule));
-    }
-
-    if (json.nonIPQuery !== undefined || json.blockTypes !== undefined) {
-        return buildLegacyDNSRules(json.nonIPQuery, json.blockTypes);
-    }
-
-    return [];
-}
-
-Object.freeze(Protocols);
-Object.freeze(SSMethods);
-Object.freeze(TLS_FLOW_CONTROL);
-Object.freeze(UTLS_FINGERPRINT);
-Object.freeze(ALPN_OPTION);
-Object.freeze(SNIFFING_OPTION);
-Object.freeze(OutboundDomainStrategies);
-Object.freeze(WireguardDomainStrategy);
-Object.freeze(USERS_SECURITY);
-Object.freeze(MODE_OPTION);
-Object.freeze(Address_Port_Strategy);
-Object.freeze(DNSRuleActions);
-
-export class CommonClass {
-    [key: string]: any;
-
-    static toJsonArray(arr: any[]): any[] {
-        return arr.map(obj => obj.toJson());
-    }
-
-    static fromJson(..._args: any[]): any {
-        return new CommonClass();
-    }
-
-    toJson(): any {
-        return this;
-    }
-
-    toString(format: boolean = true): string {
-        return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
-    }
-}
-
-export class ReverseSniffing extends CommonClass {
-    constructor(
-        enabled = false,
-        destOverride = ['http', 'tls', 'quic', 'fakedns'],
-        metadataOnly = false,
-        routeOnly = false,
-        ipsExcluded = [],
-        domainsExcluded = [],
-    ) {
-        super();
-        this.enabled = enabled;
-        this.destOverride = Array.isArray(destOverride) && destOverride.length > 0 ? destOverride : ['http', 'tls', 'quic', 'fakedns'];
-        this.metadataOnly = metadataOnly;
-        this.routeOnly = routeOnly;
-        this.ipsExcluded = Array.isArray(ipsExcluded) ? ipsExcluded : [];
-        this.domainsExcluded = Array.isArray(domainsExcluded) ? domainsExcluded : [];
-    }
-
-    static fromJson(json: any = {}): any {
-        if (!json || Object.keys(json).length === 0) {
-            return new ReverseSniffing();
-        }
-        return new ReverseSniffing(
-            !!json.enabled,
-            json.destOverride,
-            json.metadataOnly,
-            json.routeOnly,
-            json.ipsExcluded || [],
-            json.domainsExcluded || [],
-        );
-    }
-
-    toJson() {
-        return {
-            enabled: this.enabled,
-            destOverride: this.destOverride,
-            metadataOnly: this.metadataOnly,
-            routeOnly: this.routeOnly,
-            ipsExcluded: this.ipsExcluded.length > 0 ? this.ipsExcluded : undefined,
-            domainsExcluded: this.domainsExcluded.length > 0 ? this.domainsExcluded : undefined,
-        };
-    }
-}
-
-export class TcpStreamSettings extends CommonClass {
-    constructor(type: any = 'none', host?: any, path?: any) {
-        super();
-        this.type = type;
-        this.host = host;
-        this.path = path;
-    }
-
-    static fromJson(json: any = {}): any {
-        const header = json.header;
-        if (!header) return new TcpStreamSettings();
-        if (header.type == 'http' && header.request) {
-            return new TcpStreamSettings(
-                header.type,
-                header.request.headers.Host.join(','),
-                header.request.path.join(','),
-            );
-        }
-        return new TcpStreamSettings(header.type, '', '');
-    }
-
-    toJson() {
-        return {
-            header: {
-                type: this.type,
-                request: this.type === 'http' ? {
-                    headers: {
-                        Host: ObjectUtil.isEmpty(this.host) ? [] : this.host.split(',')
-                    },
-                    path: ObjectUtil.isEmpty(this.path) ? ["/"] : this.path.split(',')
-                } : undefined,
-            }
-        };
-    }
-}
-
-export class KcpStreamSettings extends CommonClass {
-    constructor(
-        mtu = 1350,
-        tti = 20,
-        uplinkCapacity = 5,
-        downlinkCapacity = 20,
-        cwndMultiplier = 1,
-        maxSendingWindow = 1350,
-    ) {
-        super();
-        this.mtu = mtu;
-        this.tti = tti;
-        this.upCap = uplinkCapacity;
-        this.downCap = downlinkCapacity;
-        this.cwndMultiplier = cwndMultiplier;
-        this.maxSendingWindow = maxSendingWindow;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new KcpStreamSettings(
-            json.mtu,
-            json.tti,
-            json.uplinkCapacity,
-            json.downlinkCapacity,
-            json.cwndMultiplier,
-            json.maxSendingWindow,
-        );
-    }
-
-    toJson() {
-        return {
-            mtu: this.mtu,
-            tti: this.tti,
-            uplinkCapacity: this.upCap,
-            downlinkCapacity: this.downCap,
-            cwndMultiplier: this.cwndMultiplier,
-            maxSendingWindow: this.maxSendingWindow,
-        };
-    }
-}
-
-export class WsStreamSettings extends CommonClass {
-    constructor(
-        path = '/',
-        host = '',
-        heartbeatPeriod = 0,
-
-    ) {
-        super();
-        this.path = path;
-        this.host = host;
-        this.heartbeatPeriod = heartbeatPeriod;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new WsStreamSettings(
-            json.path,
-            json.host,
-            json.heartbeatPeriod,
-        );
-    }
-
-    toJson() {
-        return {
-            path: this.path,
-            host: this.host,
-            heartbeatPeriod: this.heartbeatPeriod
-        };
-    }
-}
-
-export class GrpcStreamSettings extends CommonClass {
-    constructor(
-        serviceName = "",
-        authority = "",
-        multiMode = false
-    ) {
-        super();
-        this.serviceName = serviceName;
-        this.authority = authority;
-        this.multiMode = multiMode;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new GrpcStreamSettings(json.serviceName, json.authority, json.multiMode);
-    }
-
-    toJson() {
-        return {
-            serviceName: this.serviceName,
-            authority: this.authority,
-            multiMode: this.multiMode
-        }
-    }
-}
-
-export class HttpUpgradeStreamSettings extends CommonClass {
-    constructor(path = '/', host = '') {
-        super();
-        this.path = path;
-        this.host = host;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new HttpUpgradeStreamSettings(
-            json.path,
-            json.host,
-        );
-    }
-
-    toJson() {
-        return {
-            path: this.path,
-            host: this.host,
-        };
-    }
-}
-
-// Mirrors the outbound (client-side) view of Xray-core's SplitHTTPConfig
-// (infra/conf/transport_internet.go). Only fields the client actually
-// reads at runtime, plus the bidirectional fields the client must match
-// against the server, live here. Server-only fields (noSSEHeader,
-// scMaxBufferedPosts, scStreamUpServerSecs, serverMaxHeaderBytes) belong
-// on the inbound class instead.
-export class xHTTPStreamSettings extends CommonClass {
-    constructor(
-        // Bidirectional — must match the inbound side
-        path: any = '/',
-        host: any = '',
-        mode: any = '',
-        xPaddingBytes: any = "100-1000",
-        xPaddingObfsMode = false,
-        xPaddingKey = '',
-        xPaddingHeader = '',
-        xPaddingPlacement = '',
-        xPaddingMethod = '',
-        sessionPlacement = '',
-        sessionKey = '',
-        seqPlacement = '',
-        seqKey = '',
-        uplinkDataPlacement = '',
-        uplinkDataKey = '',
-        scMaxEachPostBytes: any = "1000000",
-        // Client-side only
-        headers: any[] = [],
-        uplinkHTTPMethod = '',
-        uplinkChunkSize = 0,
-        noGRPCHeader = false,
-        scMinPostsIntervalMs = "30",
-        xmux = {
-            maxConcurrency: "16-32",
-            maxConnections: 0,
-            cMaxReuseTimes: 0,
-            hMaxRequestTimes: "600-900",
-            hMaxReusableSecs: "1800-3000",
-            hKeepAlivePeriod: 0,
-        },
-        // UI-only toggle — controls whether the XMUX block is expanded in
-        // the form (mirrors the QUIC Params switch in stream_finalmask).
-        // Never serialized; toJson() only emits the xmux block itself.
-        enableXmux = false,
-    ) {
-        super();
-        this.path = path;
-        this.host = host;
-        this.mode = mode;
-        this.xPaddingBytes = xPaddingBytes;
-        this.xPaddingObfsMode = xPaddingObfsMode;
-        this.xPaddingKey = xPaddingKey;
-        this.xPaddingHeader = xPaddingHeader;
-        this.xPaddingPlacement = xPaddingPlacement;
-        this.xPaddingMethod = xPaddingMethod;
-        this.sessionPlacement = sessionPlacement;
-        this.sessionKey = sessionKey;
-        this.seqPlacement = seqPlacement;
-        this.seqKey = seqKey;
-        this.uplinkDataPlacement = uplinkDataPlacement;
-        this.uplinkDataKey = uplinkDataKey;
-        this.scMaxEachPostBytes = scMaxEachPostBytes;
-        this.headers = headers;
-        this.uplinkHTTPMethod = uplinkHTTPMethod;
-        this.uplinkChunkSize = uplinkChunkSize;
-        this.noGRPCHeader = noGRPCHeader;
-        this.scMinPostsIntervalMs = scMinPostsIntervalMs;
-        this.xmux = xmux;
-        this.enableXmux = enableXmux;
-    }
-
-    addHeader(name: any, value: any): void {
-        this.headers.push({ name: name, value: value });
-    }
-
-    removeHeader(index: number): void {
-        this.headers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}): any {
-        const headersInput = json.headers;
-        let headers: any[] = [];
-        if (Array.isArray(headersInput)) {
-            headers = headersInput;
-        } else if (headersInput && typeof headersInput === 'object') {
-            // Upstream uses a {name: value} map; convert to the panel's [{name, value}] form.
-            headers = Object.entries(headersInput).map(([name, value]) => ({ name, value }));
-        }
-        return new xHTTPStreamSettings(
-            json.path,
-            json.host,
-            json.mode,
-            json.xPaddingBytes,
-            json.xPaddingObfsMode,
-            json.xPaddingKey,
-            json.xPaddingHeader,
-            json.xPaddingPlacement,
-            json.xPaddingMethod,
-            json.sessionPlacement,
-            json.sessionKey,
-            json.seqPlacement,
-            json.seqKey,
-            json.uplinkDataPlacement,
-            json.uplinkDataKey,
-            json.scMaxEachPostBytes,
-            headers,
-            json.uplinkHTTPMethod,
-            json.uplinkChunkSize,
-            json.noGRPCHeader,
-            json.scMinPostsIntervalMs,
-            json.xmux,
-            // Auto-toggle the XMUX switch on when an existing outbound has
-            // the xmux key saved, so users editing such configs see their
-            // values immediately.
-            json.xmux !== undefined,
-        );
-    }
-
-    toJson() {
-        // Upstream expects headers as a {name: value} map, not a list of entries.
-        const headersMap: any = {};
-        if (Array.isArray(this.headers)) {
-            for (const h of this.headers) {
-                if (h && h.name) headersMap[h.name] = h.value || '';
-            }
-        }
-        return {
-            path: this.path,
-            host: this.host,
-            mode: this.mode,
-            xPaddingBytes: this.xPaddingBytes,
-            xPaddingObfsMode: this.xPaddingObfsMode,
-            xPaddingKey: this.xPaddingKey,
-            xPaddingHeader: this.xPaddingHeader,
-            xPaddingPlacement: this.xPaddingPlacement,
-            xPaddingMethod: this.xPaddingMethod,
-            sessionPlacement: this.sessionPlacement,
-            sessionKey: this.sessionKey,
-            seqPlacement: this.seqPlacement,
-            seqKey: this.seqKey,
-            uplinkDataPlacement: this.uplinkDataPlacement,
-            uplinkDataKey: this.uplinkDataKey,
-            scMaxEachPostBytes: this.scMaxEachPostBytes,
-            headers: headersMap,
-            uplinkHTTPMethod: this.uplinkHTTPMethod,
-            uplinkChunkSize: this.uplinkChunkSize,
-            noGRPCHeader: this.noGRPCHeader,
-            scMinPostsIntervalMs: this.scMinPostsIntervalMs,
-            xmux: {
-                maxConcurrency: this.xmux.maxConcurrency,
-                maxConnections: this.xmux.maxConnections,
-                cMaxReuseTimes: this.xmux.cMaxReuseTimes,
-                hMaxRequestTimes: this.xmux.hMaxRequestTimes,
-                hMaxReusableSecs: this.xmux.hMaxReusableSecs,
-                hKeepAlivePeriod: this.xmux.hKeepAlivePeriod,
-            },
-        };
-    }
-}
-
-export class TlsStreamSettings extends CommonClass {
-    constructor(
-        serverName: any = '',
-        alpn: any[] = [],
-        fingerprint: any = '',
-        echConfigList = '',
-        verifyPeerCertByName = '',
-        pinnedPeerCertSha256 = '',
-    ) {
-        super();
-        this.serverName = serverName;
-        this.alpn = alpn;
-        this.fingerprint = fingerprint;
-        this.echConfigList = echConfigList;
-        this.verifyPeerCertByName = verifyPeerCertByName;
-        this.pinnedPeerCertSha256 = pinnedPeerCertSha256;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new TlsStreamSettings(
-            json.serverName,
-            json.alpn,
-            json.fingerprint,
-            json.echConfigList,
-            json.verifyPeerCertByName,
-            json.pinnedPeerCertSha256,
-        );
-    }
-
-    toJson() {
-        return {
-            serverName: this.serverName,
-            alpn: this.alpn,
-            fingerprint: this.fingerprint,
-            echConfigList: this.echConfigList,
-            verifyPeerCertByName: this.verifyPeerCertByName,
-            pinnedPeerCertSha256: this.pinnedPeerCertSha256
-        };
-    }
-}
-
-export class RealityStreamSettings extends CommonClass {
-    constructor(
-        publicKey: any = '',
-        fingerprint: any = '',
-        serverName: any = '',
-        shortId: any = '',
-        spiderX: any = '',
-        mldsa65Verify: any = ''
-    ) {
-        super();
-        this.publicKey = publicKey;
-        this.fingerprint = fingerprint;
-        this.serverName = serverName;
-        this.shortId = shortId
-        this.spiderX = spiderX;
-        this.mldsa65Verify = mldsa65Verify;
-    }
-    static fromJson(json: any = {}): any {
-        return new RealityStreamSettings(
-            json.publicKey,
-            json.fingerprint,
-            json.serverName,
-            json.shortId,
-            json.spiderX,
-            json.mldsa65Verify
-        );
-    }
-    toJson() {
-        return {
-            publicKey: this.publicKey,
-            fingerprint: this.fingerprint,
-            serverName: this.serverName,
-            shortId: this.shortId,
-            spiderX: this.spiderX,
-            mldsa65Verify: this.mldsa65Verify
-        };
-    }
-};
-
-export class HysteriaStreamSettings extends CommonClass {
-    constructor(
-        version = 2,
-        auth = '',
-        congestion = '',
-        up = '0',
-        down = '0',
-        udphopPort = '',
-        udphopIntervalMin = 30,
-        udphopIntervalMax = 30,
-        initStreamReceiveWindow = 8388608,
-        maxStreamReceiveWindow = 8388608,
-        initConnectionReceiveWindow = 20971520,
-        maxConnectionReceiveWindow = 20971520,
-        maxIdleTimeout = 30,
-        keepAlivePeriod = 2,
-        disablePathMTUDiscovery = false
-    ) {
-        super();
-        this.version = version;
-        this.auth = auth;
-        this.congestion = congestion;
-        this.up = up;
-        this.down = down;
-        this.udphopPort = udphopPort;
-        this.udphopIntervalMin = udphopIntervalMin;
-        this.udphopIntervalMax = udphopIntervalMax;
-        this.initStreamReceiveWindow = initStreamReceiveWindow;
-        this.maxStreamReceiveWindow = maxStreamReceiveWindow;
-        this.initConnectionReceiveWindow = initConnectionReceiveWindow;
-        this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
-        this.maxIdleTimeout = maxIdleTimeout;
-        this.keepAlivePeriod = keepAlivePeriod;
-        this.disablePathMTUDiscovery = disablePathMTUDiscovery;
-    }
-
-    static fromJson(json: any = {}): any {
-        let udphopPort = '';
-        let udphopIntervalMin = 30;
-        let udphopIntervalMax = 30;
-        if (json.udphop) {
-            udphopPort = json.udphop.port || '';
-            // Backward compatibility: if old 'interval' exists, use it for both min/max
-            if (json.udphop.interval !== undefined) {
-                udphopIntervalMin = json.udphop.interval;
-                udphopIntervalMax = json.udphop.interval;
-            } else {
-                udphopIntervalMin = json.udphop.intervalMin || 30;
-                udphopIntervalMax = json.udphop.intervalMax || 30;
-            }
-        }
-        return new HysteriaStreamSettings(
-            json.version,
-            json.auth,
-            json.congestion,
-            json.up,
-            json.down,
-            udphopPort,
-            udphopIntervalMin,
-            udphopIntervalMax,
-            json.initStreamReceiveWindow,
-            json.maxStreamReceiveWindow,
-            json.initConnectionReceiveWindow,
-            json.maxConnectionReceiveWindow,
-            json.maxIdleTimeout,
-            json.keepAlivePeriod,
-            json.disablePathMTUDiscovery
-        );
-    }
-
-    toJson() {
-        const result: any = {
-            version: this.version,
-            auth: this.auth,
-            congestion: this.congestion,
-            up: this.up,
-            down: this.down,
-            initStreamReceiveWindow: this.initStreamReceiveWindow,
-            maxStreamReceiveWindow: this.maxStreamReceiveWindow,
-            initConnectionReceiveWindow: this.initConnectionReceiveWindow,
-            maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,
-            maxIdleTimeout: this.maxIdleTimeout,
-            keepAlivePeriod: this.keepAlivePeriod,
-            disablePathMTUDiscovery: this.disablePathMTUDiscovery
-        };
-        if (this.udphopPort) {
-            result.udphop = {
-                port: this.udphopPort,
-                intervalMin: this.udphopIntervalMin,
-                intervalMax: this.udphopIntervalMax
-            };
-        }
-        return result;
-    }
-};
-export class SockoptStreamSettings extends CommonClass {
-    constructor(
-        dialerProxy = "",
-        tcpFastOpen = false,
-        tcpKeepAliveInterval = 0,
-        tcpMptcp = false,
-        penetrate = false,
-        addressPortStrategy = Address_Port_Strategy.NONE,
-        trustedXForwardedFor = [],
-        mark = 0,            
-        interfaceName = "",  
-
-    ) {
-        super();
-        this.dialerProxy = dialerProxy;
-        this.tcpFastOpen = tcpFastOpen;
-        this.tcpKeepAliveInterval = tcpKeepAliveInterval;
-        this.tcpMptcp = tcpMptcp;
-        this.penetrate = penetrate;
-        this.addressPortStrategy = addressPortStrategy;
-        this.trustedXForwardedFor = trustedXForwardedFor;
-        this.mark = mark;          
-        this.interfaceName = interfaceName; 
-
-    }
-
-    static fromJson(json: any = {}): any {
-        if (Object.keys(json).length === 0) return undefined;
-        return new SockoptStreamSettings(
-            json.dialerProxy,
-            json.tcpFastOpen,
-            json.tcpKeepAliveInterval,
-            json.tcpMptcp,
-            json.penetrate,
-            json.addressPortStrategy,
-            json.trustedXForwardedFor || [],
-            json.mark ?? 0,      
-            json.interface ?? "", 
-        );
-    }
-
-    toJson() {
-        const result: any = {
-            dialerProxy: this.dialerProxy,
-            tcpFastOpen: this.tcpFastOpen,
-            tcpKeepAliveInterval: this.tcpKeepAliveInterval,
-            tcpMptcp: this.tcpMptcp,
-            penetrate: this.penetrate,
-            addressPortStrategy: this.addressPortStrategy,
-            mark: this.mark, 
-            interface: this.interfaceName, 
-        };
-        if (this.trustedXForwardedFor && this.trustedXForwardedFor.length > 0) {
-            result.trustedXForwardedFor = this.trustedXForwardedFor;
-        }
-        return result;
-    }
-}
-
-export class UdpMask extends CommonClass {
-    constructor(type: any = 'salamander', settings: any = {}) {
-        super();
-        this.type = type;
-        this.settings = this._getDefaultSettings(type, settings);
-    }
-
-    _getDefaultSettings(type: any, settings: any = {}): any {
-        switch (type) {
-            case 'salamander':
-            case 'mkcp-aes128gcm':
-                return { password: settings.password || '' };
-            case 'header-dns':
-                return { domain: settings.domain || '' };
-            case 'xdns':
-                return { resolvers: Array.isArray(settings.resolvers) ? settings.resolvers : [] };
-            case 'xicmp':
-                return { ip: settings.ip || '', id: settings.id ?? 0 };
-            case 'mkcp-original':
-            case 'header-dtls':
-            case 'header-srtp':
-            case 'header-utp':
-            case 'header-wechat':
-            case 'header-wireguard':
-                return {}; // No settings needed
-            case 'header-custom':
-                return {
-                    client: Array.isArray(settings.client) ? settings.client : [],
-                    server: Array.isArray(settings.server) ? settings.server : [],
-                };
-            case 'noise':
-                return {
-                    reset: settings.reset ?? 0,
-                    noise: Array.isArray(settings.noise) ? settings.noise : [],
-                };
-            case 'sudoku':
-                return {
-                    ascii: settings.ascii || '',
-                    customTable: settings.customTable || '',
-                    customTables: Array.isArray(settings.customTables) ? settings.customTables : [],
-                    paddingMin: settings.paddingMin ?? 0,
-                    paddingMax: settings.paddingMax ?? 0
-                };
-            default:
-                return settings;
-        }
-    }
-
-    static fromJson(json: any = {}): any {
-        return new UdpMask(
-            json.type || 'salamander',
-            json.settings || {}
-        );
-    }
-
-    toJson() {
-        const cleanItem = (item: any) => {
-            const out = { ...item };
-            if (out.type === 'array') {
-                delete out.packet;
-            } else {
-                delete out.rand;
-                delete out.randRange;
-            }
-            return out;
-        };
-
-        let settings = this.settings;
-        if (this.type === 'noise' && settings && Array.isArray(settings.noise)) {
-            settings = { ...settings, noise: settings.noise.map(cleanItem) };
-        } else if (this.type === 'header-custom' && settings) {
-            settings = {
-                ...settings,
-                client: Array.isArray(settings.client) ? settings.client.map(cleanItem) : settings.client,
-                server: Array.isArray(settings.server) ? settings.server.map(cleanItem) : settings.server,
-            };
-        }
-
-        return {
-            type: this.type,
-            settings: (settings && Object.keys(settings).length > 0) ? settings : undefined
-        };
-    }
-}
-
-export class TcpMask extends CommonClass {
-    constructor(type: any = 'fragment', settings: any = {}) {
-        super();
-        this.type = type;
-        this.settings = this._getDefaultSettings(type, settings);
-    }
-
-    _getDefaultSettings(type: any, settings: any = {}): any {
-        switch (type) {
-            case 'fragment':
-                return {
-                    packets: settings.packets ?? 'tlshello',
-                    length: settings.length ?? '',
-                    delay: settings.delay ?? '',
-                    maxSplit: settings.maxSplit ?? '',
-                };
-            case 'sudoku':
-                return {
-                    password: settings.password ?? '',
-                    ascii: settings.ascii ?? '',
-                    customTable: settings.customTable ?? '',
-                    customTables: Array.isArray(settings.customTables) ? settings.customTables : [],
-                    paddingMin: settings.paddingMin ?? 0,
-                    paddingMax: settings.paddingMax ?? 0,
-                };
-            case 'header-custom':
-                return {
-                    clients: Array.isArray(settings.clients) ? settings.clients : [],
-                    servers: Array.isArray(settings.servers) ? settings.servers : [],
-                };
-            default:
-                return settings;
-        }
-    }
-
-    static fromJson(json: any = {}): any {
-        return new TcpMask(
-            json.type || 'fragment',
-            json.settings || {}
-        );
-    }
-
-    toJson() {
-        const cleanItem = (item: any) => {
-            const out = { ...item };
-            if (out.type === 'array') {
-                delete out.packet;
-            } else {
-                delete out.rand;
-                delete out.randRange;
-            }
-            return out;
-        };
-
-        let settings = this.settings;
-        if (this.type === 'header-custom' && settings) {
-            const cleanGroup = (group: any) => Array.isArray(group) ? group.map(cleanItem) : group;
-            settings = {
-                ...settings,
-                clients: Array.isArray(settings.clients) ? settings.clients.map(cleanGroup) : settings.clients,
-                servers: Array.isArray(settings.servers) ? settings.servers.map(cleanGroup) : settings.servers,
-            };
-        }
-
-        return {
-            type: this.type,
-            settings: (settings && Object.keys(settings).length > 0) ? settings : undefined
-        };
-    }
-}
-
-export class QuicParams extends CommonClass {
-    constructor(
-        congestion: any = 'bbr',
-        debug: any = false,
-        brutalUp: any = 65537,
-        brutalDown: any = 65537,
-        udpHop: any = undefined,
-        initStreamReceiveWindow = 8388608,
-        maxStreamReceiveWindow = 8388608,
-        initConnectionReceiveWindow = 20971520,
-        maxConnectionReceiveWindow = 20971520,
-        maxIdleTimeout = 30,
-        keepAlivePeriod = 5,
-        disablePathMTUDiscovery = false,
-        maxIncomingStreams = 1024,
-    ) {
-        super();
-        this.congestion = congestion;
-        this.debug = debug;
-        this.brutalUp = brutalUp;
-        this.brutalDown = brutalDown;
-        this.udpHop = udpHop;
-        this.initStreamReceiveWindow = initStreamReceiveWindow;
-        this.maxStreamReceiveWindow = maxStreamReceiveWindow;
-        this.initConnectionReceiveWindow = initConnectionReceiveWindow;
-        this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
-        this.maxIdleTimeout = maxIdleTimeout;
-        this.keepAlivePeriod = keepAlivePeriod;
-        this.disablePathMTUDiscovery = disablePathMTUDiscovery;
-        this.maxIncomingStreams = maxIncomingStreams;
-    }
-
-    get hasUdpHop() {
-        return this.udpHop != null;
-    }
-
-    set hasUdpHop(value) {
-        this.udpHop = value ? (this.udpHop || { ports: '20000-50000', interval: '5-10' }) : undefined;
-    }
-
-    static fromJson(json: any = {}): any {
-        if (!json || Object.keys(json).length === 0) return undefined;
-        return new QuicParams(
-            json.congestion,
-            json.debug,
-            json.brutalUp,
-            json.brutalDown,
-            json.udpHop ? { ports: json.udpHop.ports, interval: json.udpHop.interval } : undefined,
-            json.initStreamReceiveWindow,
-            json.maxStreamReceiveWindow,
-            json.initConnectionReceiveWindow,
-            json.maxConnectionReceiveWindow,
-            json.maxIdleTimeout,
-            json.keepAlivePeriod,
-            json.disablePathMTUDiscovery,
-            json.maxIncomingStreams,
-        );
-    }
-
-    toJson() {
-        const result: any = { congestion: this.congestion } as any;
-        if (this.debug) result.debug = this.debug;
-        if (['brutal', 'force-brutal'].includes(this.congestion)) {
-            if (this.brutalUp) result.brutalUp = this.brutalUp;
-            if (this.brutalDown) result.brutalDown = this.brutalDown;
-        }
-        if (this.udpHop) result.udpHop = { ports: this.udpHop.ports, interval: this.udpHop.interval };
-        if (this.initStreamReceiveWindow > 0) result.initStreamReceiveWindow = this.initStreamReceiveWindow;
-        if (this.maxStreamReceiveWindow > 0) result.maxStreamReceiveWindow = this.maxStreamReceiveWindow;
-        if (this.initConnectionReceiveWindow > 0) result.initConnectionReceiveWindow = this.initConnectionReceiveWindow;
-        if (this.maxConnectionReceiveWindow > 0) result.maxConnectionReceiveWindow = this.maxConnectionReceiveWindow;
-        if (this.maxIdleTimeout !== 30 && this.maxIdleTimeout > 0) result.maxIdleTimeout = this.maxIdleTimeout;
-        if (this.keepAlivePeriod > 0) result.keepAlivePeriod = this.keepAlivePeriod;
-        if (this.disablePathMTUDiscovery) result.disablePathMTUDiscovery = this.disablePathMTUDiscovery;
-        if (this.maxIncomingStreams > 0) result.maxIncomingStreams = this.maxIncomingStreams;
-        return result;
-    }
-}
-
-export class FinalMaskStreamSettings extends CommonClass {
-    constructor(tcp: any[] = [], udp: any[] = [], quicParams: any = undefined) {
-        super();
-        this.tcp = Array.isArray(tcp) ? tcp.map((t: any) => t instanceof TcpMask ? t : new TcpMask(t.type, t.settings)) : [];
-        this.udp = Array.isArray(udp) ? udp.map((u: any) => new UdpMask(u.type, u.settings)) : [new UdpMask((udp as any).type, (udp as any).settings)];
-        this.quicParams = quicParams instanceof QuicParams ? quicParams : (quicParams ? QuicParams.fromJson(quicParams) : undefined);
-    }
-
-    get enableQuicParams() {
-        return this.quicParams != null;
-    }
-
-    set enableQuicParams(value) {
-        this.quicParams = value ? (this.quicParams || new QuicParams()) : undefined;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new FinalMaskStreamSettings(
-            json.tcp || [],
-            json.udp || [],
-            json.quicParams ? QuicParams.fromJson(json.quicParams) : undefined,
-        );
-    }
-
-    toJson() {
-        const result: any = {} as any;
-        if (this.tcp && this.tcp.length > 0) {
-            result.tcp = this.tcp.map((t: any) => t.toJson());
-        }
-        if (this.udp && this.udp.length > 0) {
-            result.udp = this.udp.map((udp: any) => udp.toJson());
-        }
-        if (this.quicParams) {
-            result.quicParams = this.quicParams.toJson();
-        }
-        return result;
-    }
-}
-
-export class StreamSettings extends CommonClass {
-    constructor(
-        network = 'tcp',
-        security = 'none',
-        tlsSettings = new TlsStreamSettings(),
-        realitySettings = new RealityStreamSettings(),
-        tcpSettings = new TcpStreamSettings(),
-        kcpSettings = new KcpStreamSettings(),
-        wsSettings = new WsStreamSettings(),
-        grpcSettings = new GrpcStreamSettings(),
-        httpupgradeSettings = new HttpUpgradeStreamSettings(),
-        xhttpSettings = new xHTTPStreamSettings(),
-        hysteriaSettings = new HysteriaStreamSettings(),
-        finalmask = new FinalMaskStreamSettings(),
-        sockopt = undefined,
-    ) {
-        super();
-        this.network = network;
-        this.security = security;
-        this.tls = tlsSettings;
-        this.reality = realitySettings;
-        this.tcp = tcpSettings;
-        this.kcp = kcpSettings;
-        this.ws = wsSettings;
-        this.grpc = grpcSettings;
-        this.httpupgrade = httpupgradeSettings;
-        this.xhttp = xhttpSettings;
-        this.hysteria = hysteriaSettings;
-        this.finalmask = finalmask;
-        this.sockopt = sockopt;
-    }
-
-    addTcpMask(type = 'fragment') {
-        this.finalmask.tcp.push(new TcpMask(type));
-    }
-
-    delTcpMask(index: number) {
-        if (this.finalmask.tcp) {
-            this.finalmask.tcp.splice(index, 1);
-        }
-    }
-
-    addUdpMask(type = 'salamander') {
-        this.finalmask.udp.push(new UdpMask(type));
-    }
-
-    delUdpMask(index: number) {
-        if (this.finalmask.udp) {
-            this.finalmask.udp.splice(index, 1);
-        }
-    }
-
-    get hasFinalMask() {
-        const hasTcp = this.finalmask.tcp && this.finalmask.tcp.length > 0;
-        const hasUdp = this.finalmask.udp && this.finalmask.udp.length > 0;
-        const hasQuicParams = this.finalmask.quicParams != null;
-        return hasTcp || hasUdp || hasQuicParams;
-    }
-
-    get isTls() {
-        return this.security === 'tls';
-    }
-
-    get isReality() {
-        return this.security === "reality";
-    }
-
-    get sockoptSwitch() {
-        return this.sockopt != undefined;
-    }
-
-    set sockoptSwitch(value) {
-        this.sockopt = value ? new SockoptStreamSettings() : undefined;
-    }
-
-    static fromJson(json: any = {}): any {
-        // Xray-core supports both "xhttpSettings" and "splithttpSettings" (backward-compat alias)
-        const xhttpJson = json.xhttpSettings ?? json.splithttpSettings;
-        // Normalize "splithttp" network name to "xhttp" for internal consistency
-        const network = json.network === 'splithttp' ? 'xhttp' : json.network;
-        return new StreamSettings(
-            network,
-            json.security,
-            TlsStreamSettings.fromJson(json.tlsSettings),
-            RealityStreamSettings.fromJson(json.realitySettings),
-            TcpStreamSettings.fromJson(json.tcpSettings),
-            KcpStreamSettings.fromJson(json.kcpSettings),
-            WsStreamSettings.fromJson(json.wsSettings),
-            GrpcStreamSettings.fromJson(json.grpcSettings),
-            HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
-            xHTTPStreamSettings.fromJson(xhttpJson),
-            HysteriaStreamSettings.fromJson(json.hysteriaSettings),
-            FinalMaskStreamSettings.fromJson(json.finalmask),
-            SockoptStreamSettings.fromJson(json.sockopt),
-        );
-    }
-
-    toJson() {
-        const network = this.network;
-        return {
-            network: network,
-            security: this.security,
-            tlsSettings: this.security == 'tls' ? this.tls.toJson() : undefined,
-            realitySettings: this.security == 'reality' ? this.reality.toJson() : undefined,
-            tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
-            kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
-            wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
-            grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
-            httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
-            xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
-            hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
-            finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
-            sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
-        };
-    }
-}
-
-export class Mux extends CommonClass {
-    constructor(enabled = false, concurrency = 8, xudpConcurrency = 16, xudpProxyUDP443 = "reject") {
-        super();
-        this.enabled = enabled;
-        this.concurrency = concurrency;
-        this.xudpConcurrency = xudpConcurrency;
-        this.xudpProxyUDP443 = xudpProxyUDP443;
-    }
-
-    static fromJson(json: any = {}): any {
-        if (Object.keys(json).length === 0) return undefined;
-        return new Mux(
-            json.enabled,
-            json.concurrency,
-            json.xudpConcurrency,
-            json.xudpProxyUDP443,
-        );
-    }
-
-    toJson() {
-        return {
-            enabled: this.enabled,
-            concurrency: this.concurrency,
-            xudpConcurrency: this.xudpConcurrency,
-            xudpProxyUDP443: this.xudpProxyUDP443,
-        };
-    }
-}
-
-export class Outbound extends CommonClass {
-    static Settings: any;
-    static FreedomSettings: any;
-    static BlackholeSettings: any;
-    static LoopbackSettings: any;
-    static DNSRule: any;
-    static DNSSettings: any;
-    static VmessSettings: any;
-    static VLESSSettings: any;
-    static TrojanSettings: any;
-    static ShadowsocksSettings: any;
-    static SocksSettings: any;
-    static HttpSettings: any;
-    static WireguardSettings: any;
-    static HysteriaSettings: any;
-
-    constructor(
-        tag: any = '',
-        protocol: any = Protocols.VLESS,
-        settings: any = null,
-        streamSettings: any = new StreamSettings(),
-        sendThrough?: any,
-        mux: any = new Mux(),
-    ) {
-        super();
-        this.tag = tag;
-        this._protocol = protocol;
-        this.settings = settings == null ? Outbound.Settings.getSettings(protocol) : settings;
-        this.stream = streamSettings;
-        this.sendThrough = sendThrough;
-        this.mux = mux;
-    }
-
-    get protocol() {
-        return this._protocol;
-    }
-
-    set protocol(protocol) {
-        this._protocol = protocol;
-        this.settings = Outbound.Settings.getSettings(protocol);
-        this.stream = new StreamSettings();
-    }
-
-    canEnableTls() {
-        if (this.protocol === Protocols.Hysteria) return true;
-        if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
-        return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
-    }
-
-    //this is used for xtls-rprx-vision
-    canEnableTlsFlow() {
-        if ((this.stream.security != 'none') && (this.stream.network === "tcp")) {
-            return this.protocol === Protocols.VLESS;
-        }
-        return false;
-    }
-
-    // Vision seed applies only when the XTLS Vision (TCP/TLS) flow is selected.
-    // Excludes the UDP variant per spec.
-    canEnableVisionSeed() {
-        if (!this.canEnableTlsFlow()) return false;
-        return this.settings?.flow === TLS_FLOW_CONTROL.VISION;
-    }
-
-    canEnableReality() {
-        if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
-        return ["tcp", "http", "grpc", "xhttp"].includes(this.stream.network);
-    }
-
-    canEnableStream() {
-        return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol);
-    }
-
-    canEnableMux() {
-        // Disable Mux if flow is set
-        if (this.settings.flow && this.settings.flow !== '') {
-            this.mux.enabled = false;
-            return false;
-        }
-
-        // Disable Mux if network is xhttp
-        if (this.stream.network === 'xhttp') {
-            this.mux.enabled = false;
-            return false;
-        }
-
-        // Allow Mux only for these protocols
-        return [
-            Protocols.VMess,
-            Protocols.VLESS,
-            Protocols.Trojan,
-            Protocols.Shadowsocks,
-            Protocols.HTTP,
-            Protocols.Socks
-        ].includes(this.protocol);
-    }
-
-    hasServers() {
-        return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
-    }
-
-    hasAddressPort() {
-        return [
-            Protocols.VMess,
-            Protocols.VLESS,
-            Protocols.Trojan,
-            Protocols.Shadowsocks,
-            Protocols.Socks,
-            Protocols.HTTP,
-            Protocols.Hysteria
-        ].includes(this.protocol);
-    }
-
-    hasUsername() {
-        return [Protocols.Socks, Protocols.HTTP].includes(this.protocol);
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound(
-            json.tag,
-            json.protocol,
-            Outbound.Settings.fromJson(json.protocol, json.settings),
-            StreamSettings.fromJson(json.streamSettings),
-            json.sendThrough,
-            Mux.fromJson(json.mux),
-        )
-    }
-
-    toJson() {
-        let stream;
-        if (this.canEnableStream()) {
-            stream = this.stream.toJson();
-        } else {
-            if (this.stream?.sockopt)
-                stream = { sockopt: this.stream.sockopt.toJson() };
-        }
-        const settingsOut = this.settings instanceof CommonClass ? this.settings.toJson() : this.settings;
-        return {
-            protocol: this.protocol,
-            settings: settingsOut,
-            // Only include tag, streamSettings, sendThrough, mux if present and not empty
-            ...(this.tag ? { tag: this.tag } : {}),
-            ...(stream ? { streamSettings: stream } : {}),
-            ...(this.sendThrough ? { sendThrough: this.sendThrough } : {}),
-            ...(this.mux?.enabled ? { mux: this.mux } : {}),
-        };
-    }
-
-    static fromLink(link: any) {
-        const data = link.split('://');
-        if (data.length != 2) return null;
-        switch (data[0].toLowerCase()) {
-            case Protocols.VMess:
-                return this.fromVmessLink(JSON.parse(Base64.decode(data[1])));
-            case Protocols.VLESS:
-            case Protocols.Trojan:
-            case 'ss':
-                return this.fromParamLink(link);
-            case 'hysteria2':
-            case Protocols.Hysteria:
-                return this.fromHysteriaLink(link);
-            default:
-                return null;
-        }
-    }
-
-    static fromVmessLink(json: any = {}) {
-        const stream = new StreamSettings(json.net, json.tls);
-
-        const network = json.net;
-        if (network === 'tcp') {
-            stream.tcp = new TcpStreamSettings(
-                json.type,
-                json.host ?? '',
-                json.path ?? '');
-        } else if (network === 'kcp') {
-            stream.kcp = new KcpStreamSettings();
-            stream.type = json.type;
-            stream.seed = json.path;
-            const mtu = Number(json.mtu);
-            if (Number.isFinite(mtu) && mtu > 0) stream.kcp.mtu = mtu;
-            const tti = Number(json.tti);
-            if (Number.isFinite(tti) && tti > 0) stream.kcp.tti = tti;
-        } else if (network === 'ws') {
-            stream.ws = new WsStreamSettings(json.path, json.host);
-        } else if (network === 'grpc') {
-            stream.grpc = new GrpcStreamSettings(json.path, json.authority, json.type == 'multi');
-        } else if (network === 'httpupgrade') {
-            stream.httpupgrade = new HttpUpgradeStreamSettings(json.path, json.host);
-        } else if (network === 'xhttp') {
-            const xh = new xHTTPStreamSettings(json.path, json.host);
-            if (json.mode) xh.mode = json.mode;
-            if (json.type && !json.mode) xh.mode = json.type;
-            // Padding / obfuscation — sing-box families use x_padding_bytes,
-            // while the extra block carries xPaddingBytes.
-            if (json.x_padding_bytes && !json.xPaddingBytes) json.xPaddingBytes = json.x_padding_bytes;
-            if (typeof json.xPaddingBytes === 'string' && json.xPaddingBytes) xh.xPaddingBytes = json.xPaddingBytes;
-            if (json.xPaddingObfsMode === true) {
-                xh.xPaddingObfsMode = true;
-                ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach((k: string) => {
-                    if (typeof json[k] === 'string' && json[k]) xh[k] = json[k];
-                });
-            }
-            // Bidirectional string fields carried in the extra block
-            const xFields = [
-                "uplinkHTTPMethod",
-                "sessionPlacement", "sessionKey",
-                "seqPlacement", "seqKey",
-                "uplinkDataPlacement", "uplinkDataKey",
-                "scMaxEachPostBytes", "scMinPostsIntervalMs",
-            ];
-            xFields.forEach((k: string) => {
-                if (typeof json[k] === 'string' && json[k]) xh[k] = json[k];
-            });
-            if (typeof json.uplinkChunkSize === 'number' && json.uplinkChunkSize !== 0) xh.uplinkChunkSize = json.uplinkChunkSize;
-            if (typeof json.uplinkChunkSize === 'string' && json.uplinkChunkSize) xh.uplinkChunkSize = json.uplinkChunkSize;
-            if (json.noGRPCHeader === true) xh.noGRPCHeader = true;
-            if (json.xmux && typeof json.xmux === 'object') {
-                xh.xmux = json.xmux;
-                xh.enableXmux = true;
-            }
-            if (json.downloadSettings && typeof json.downloadSettings === 'object') xh.downloadSettings = json.downloadSettings;
-            // Headers — VMess extra emits them as a {name: value} map
-            if (json.headers && typeof json.headers === 'object' && !Array.isArray(json.headers)) {
-                xh.headers = Object.entries(json.headers).map(([name, value]) => ({ name, value }));
-            }
-            stream.xhttp = xh;
-        }
-
-        if (json.tls && json.tls == 'tls') {
-            stream.tls = new TlsStreamSettings(
-                json.sni,
-                json.alpn ? json.alpn.split(',') : [],
-                json.fp);
-        }
-
-        const port = json.port * 1;
-
-        // Parse fm (finalmask) JSON string — TCP/UDP masks + QUIC params from 3x-ui share links
-        if (json.fm) {
-            try {
-                stream.finalmask = FinalMaskStreamSettings.fromJson(JSON.parse(json.fm));
-            } catch (_) { /* ignore malformed fm */ }
-        }
-
-        return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, port, json.id, json.scy), stream);
-    }
-
-    static fromParamLink(link: any) {
-        const url = new URL(link);
-        const type = url.searchParams.get('type') ?? 'tcp';
-        const security = url.searchParams.get('security') ?? 'none';
-        const stream = new StreamSettings(type, security);
-
-        const headerType = url.searchParams.get('headerType') ?? undefined;
-        const host = url.searchParams.get('host') ?? undefined;
-        const path = url.searchParams.get('path') ?? undefined;
-        const seed = url.searchParams.get('seed') ?? path ?? undefined;
-        const mode = url.searchParams.get('mode') ?? undefined;
-
-        if (type === 'tcp' || type === 'none') {
-            stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path);
-        } else if (type === 'kcp') {
-            stream.kcp = new KcpStreamSettings();
-            stream.kcp.type = headerType ?? 'none';
-            stream.kcp.seed = seed;
-            const mtu = Number(url.searchParams.get('mtu'));
-            if (Number.isFinite(mtu) && mtu > 0) stream.kcp.mtu = mtu;
-            const tti = Number(url.searchParams.get('tti'));
-            if (Number.isFinite(tti) && tti > 0) stream.kcp.tti = tti;
-        } else if (type === 'ws') {
-            stream.ws = new WsStreamSettings(path, host);
-        } else if (type === 'grpc') {
-            stream.grpc = new GrpcStreamSettings(
-                url.searchParams.get('serviceName') ?? '',
-                url.searchParams.get('authority') ?? '',
-                url.searchParams.get('mode') == 'multi');
-        } else if (type === 'httpupgrade') {
-            stream.httpupgrade = new HttpUpgradeStreamSettings(path, host);
-        } else if (type === 'xhttp') {
-            // Same positional bug as in the VMess-JSON branch above:
-            // passing `mode` as the 3rd positional arg put it into the
-            // `headers` slot. Build explicitly instead.
-            const xh = new xHTTPStreamSettings(path, host);
-            if (mode) xh.mode = mode;
-            const xpb = url.searchParams.get('x_padding_bytes');
-            if (xpb) xh.xPaddingBytes = xpb;
-            const extraRaw = url.searchParams.get('extra');
-            if (extraRaw) {
-                try {
-                    const extra = JSON.parse(extraRaw);
-                    if (typeof extra.xPaddingBytes === 'string' && extra.xPaddingBytes) xh.xPaddingBytes = extra.xPaddingBytes;
-                    if (extra.xPaddingObfsMode === true) xh.xPaddingObfsMode = true;
-                    ["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach((k: string) => {
-                        if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k];
-                    });
-                    if (!xh.mode && typeof extra.mode === 'string' && extra.mode) xh.mode = extra.mode;
-                    // Bidirectional string fields carried inside the extra block
-                    const xFields = [
-                        "uplinkHTTPMethod",
-                        "sessionPlacement", "sessionKey",
-                        "seqPlacement", "seqKey",
-                        "uplinkDataPlacement", "uplinkDataKey",
-                        "scMaxEachPostBytes", "scMinPostsIntervalMs",
-                    ];
-                    xFields.forEach((k: string) => {
-                        if (typeof extra[k] === 'string' && extra[k]) xh[k] = extra[k];
-                    });
-                    if (typeof extra.uplinkChunkSize === 'number' && extra.uplinkChunkSize !== 0) xh.uplinkChunkSize = extra.uplinkChunkSize;
-                    if (typeof extra.uplinkChunkSize === 'string' && extra.uplinkChunkSize) xh.uplinkChunkSize = extra.uplinkChunkSize;
-                    if (extra.noGRPCHeader === true) xh.noGRPCHeader = true;
-                    if (extra.xmux && typeof extra.xmux === 'object') {
-                        xh.xmux = extra.xmux;
-                        xh.enableXmux = true;
-                    }
-                    if (extra.downloadSettings && typeof extra.downloadSettings === 'object') xh.downloadSettings = extra.downloadSettings;
-                    // Headers — extra emits them as a {name: value} map
-                    if (extra.headers && typeof extra.headers === 'object' && !Array.isArray(extra.headers)) {
-                        xh.headers = Object.entries(extra.headers).map(([name, value]) => ({ name, value }));
-                    }
-                } catch (_) { /* ignore malformed extra */ }
-            }
-            stream.xhttp = xh;
-        }
-
-        if (security == 'tls') {
-            const fp = url.searchParams.get('fp') ?? 'none';
-            const alpn = url.searchParams.get('alpn');
-            const sni = url.searchParams.get('sni') ?? '';
-            const ech = url.searchParams.get('ech') ?? '';
-            stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, ech);
-        }
-
-        if (security == 'reality') {
-            const pbk = url.searchParams.get('pbk');
-            const fp = url.searchParams.get('fp');
-            const sni = url.searchParams.get('sni') ?? '';
-            const sid = url.searchParams.get('sid') ?? '';
-            const spx = url.searchParams.get('spx') ?? '';
-            const pqv = url.searchParams.get('pqv') ?? '';
-            stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx, pqv);
-        }
-
-        const regex = /([^@]+):\/\/([^@]+)@(.+):(\d+)(.*)$/;
-        const match = link.match(regex);
-
-        if (!match) return null;
-        const address = match[3];
-        let protocol = match[1];
-        let userData: any = match[2];
-        let port: any = match[4];
-        port *= 1;
-        if (protocol == 'ss') {
-            protocol = 'shadowsocks';
-            userData = atob(userData).split(':');
-        }
-        let settings;
-        switch (protocol) {
-            case Protocols.VLESS:
-                settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');
-                break;
-            case Protocols.Trojan:
-                settings = new Outbound.TrojanSettings(address, port, userData);
-                break;
-            case Protocols.Shadowsocks: {
-                const method = userData.splice(0, 1)[0];
-                settings = new Outbound.ShadowsocksSettings(address, port, userData.join(":"), method, true);
-                break;
-            }
-            default:
-                return null;
-        }
-        // Parse fm (finalmask) JSON param — TCP/UDP masks + QUIC params from 3x-ui share links
-        const fmRaw = url.searchParams.get('fm');
-        if (fmRaw) {
-            try {
-                stream.finalmask = FinalMaskStreamSettings.fromJson(JSON.parse(fmRaw));
-            } catch (_) { /* ignore malformed fm */ }
-        }
-
-        let remark = decodeURIComponent(url.hash);
-        // Remove '#' from url.hash
-        remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
-        return new Outbound(remark, protocol, settings, stream);
-    }
-
-    static fromHysteriaLink(link: any) {
-        // Parse hysteria2://password@address:port[?param1=value1&param2=value2...][#remarks]
-        const regex = /^hysteria2?:\/\/([^@]+)@([^:?#]+):(\d+)([^#]*)(#.*)?$/;
-        const match = link.match(regex);
-
-        if (!match) return null;
-
-        const password = match[1];
-        const address = match[2];
-        let port: any = match[3];
-        const params = match[4];
-        const hash = match[5];
-        port = parseInt(port);
-
-        const urlParams = new URLSearchParams(params);
-
-        const security = urlParams.get('security') ?? 'none';
-        const stream = new StreamSettings('hysteria', security);
-
-        if (security === 'tls') {
-            const fp = urlParams.get('fp') ?? 'none';
-            const alpn = urlParams.get('alpn');
-            const sni = urlParams.get('sni') ?? '';
-            const ech = urlParams.get('ech') ?? '';
-            stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, ech);
-        }
-
-        // Set hysteria stream settings
-        stream.hysteria.auth = password;
-        stream.hysteria.congestion = urlParams.get('congestion') ?? '';
-        stream.hysteria.up = urlParams.get('up') ?? '0';
-        stream.hysteria.down = urlParams.get('down') ?? '0';
-        stream.hysteria.udphopPort = urlParams.get('udphopPort') ?? '';
-        // Support both old single interval and new min/max range
-        if (urlParams.has('udphopInterval')) {
-            const interval = parseInt(urlParams.get('udphopInterval')!);
-            stream.hysteria.udphopIntervalMin = interval;
-            stream.hysteria.udphopIntervalMax = interval;
-        } else {
-            stream.hysteria.udphopIntervalMin = parseInt(urlParams.get('udphopIntervalMin') ?? '30');
-            stream.hysteria.udphopIntervalMax = parseInt(urlParams.get('udphopIntervalMax') ?? '30');
-        }
-
-        // Optional QUIC parameters for FinalMask support and hysteria2 share links
-        if (urlParams.has('initStreamReceiveWindow')) {
-            stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow')!);
-        }
-        if (urlParams.has('maxStreamReceiveWindow')) {
-            stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow')!);
-        }
-        if (urlParams.has('initConnectionReceiveWindow')) {
-            stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow')!);
-        }
-        if (urlParams.has('maxConnectionReceiveWindow')) {
-            stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow')!);
-        }
-        if (urlParams.has('maxIdleTimeout')) {
-            stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout')!);
-        }
-        if (urlParams.has('keepAlivePeriod')) {
-            stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod')!);
-        }
-        if (urlParams.has('disablePathMTUDiscovery')) {
-            stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true';
-        }
-
-        // Parse fm (finalmask) JSON param — TCP/UDP masks + QUIC params from 3x-ui share links, with special handling to mirror QUIC params into both stream.finalmask and stream.hysteria
-        const fmRaw = urlParams.get('fm');
-        if (fmRaw) {
-            try {
-                const fm = JSON.parse(fmRaw);
-                const qp = fm.quicParams;
-                if (qp && typeof qp === 'object') {
-                    // Populate stream.finalmask.quicParams — this enables the "QUIC Params"
-                    // toggle in FinalMaskForm and carries all QUIC tuning settings.
-                    stream.finalmask.quicParams = QuicParams.fromJson(qp);
-
-                    // Also mirror the overlapping fields into stream.hysteria so the
-                    // Hysteria transport section of the form shows consistent values.
-                    if (qp.congestion) stream.hysteria.congestion = qp.congestion;
-                    if (Number.isInteger(qp.initStreamReceiveWindow)) stream.hysteria.initStreamReceiveWindow = qp.initStreamReceiveWindow;
-                    if (Number.isInteger(qp.maxStreamReceiveWindow)) stream.hysteria.maxStreamReceiveWindow = qp.maxStreamReceiveWindow;
-                    if (Number.isInteger(qp.initConnectionReceiveWindow)) stream.hysteria.initConnectionReceiveWindow = qp.initConnectionReceiveWindow;
-                    if (Number.isInteger(qp.maxConnectionReceiveWindow)) stream.hysteria.maxConnectionReceiveWindow = qp.maxConnectionReceiveWindow;
-                    if (Number.isInteger(qp.maxIdleTimeout)) stream.hysteria.maxIdleTimeout = qp.maxIdleTimeout;
-                    if (Number.isInteger(qp.keepAlivePeriod)) stream.hysteria.keepAlivePeriod = qp.keepAlivePeriod;
-                    if (qp.disablePathMTUDiscovery === true) stream.hysteria.disablePathMTUDiscovery = true;
-                    if (qp.udpHop) {
-                        stream.hysteria.udphopPort = qp.udpHop.ports ?? stream.hysteria.udphopPort;
-                        if (qp.udpHop.interval !== undefined) {
-                            stream.hysteria.udphopIntervalMin = qp.udpHop.interval;
-                            stream.hysteria.udphopIntervalMax = qp.udpHop.interval;
-                        }
-                    }
-                }
-            } catch (_) { /* ignore malformed fm */ }
-        }
-
-        const settings = new Outbound.HysteriaSettings(address, port, 2);
-
-        const remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`;
-
-        return new Outbound(remark, Protocols.Hysteria, settings, stream);
-    }
-}
-
-Outbound.Settings = class extends CommonClass {
-    constructor(protocol: any) {
-        super();
-        this.protocol = protocol;
-    }
-
-    static getSettings(protocol: any): any {
-        switch (protocol) {
-            case Protocols.Freedom: return new Outbound.FreedomSettings();
-            case Protocols.Blackhole: return new Outbound.BlackholeSettings();
-            case Protocols.DNS: return new Outbound.DNSSettings();
-            case Protocols.VMess: return new Outbound.VmessSettings();
-            case Protocols.VLESS: return new Outbound.VLESSSettings();
-            case Protocols.Trojan: return new Outbound.TrojanSettings();
-            case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();
-            case Protocols.Socks: return new Outbound.SocksSettings();
-            case Protocols.HTTP: return new Outbound.HttpSettings();
-            case Protocols.Wireguard: return new Outbound.WireguardSettings();
-            case Protocols.Hysteria: return new Outbound.HysteriaSettings();
-            case Protocols.Loopback: return new Outbound.LoopbackSettings();
-            default: return null;
-        }
-    }
-
-    static fromJson(protocol: any, json: any): any {
-        switch (protocol) {
-            case Protocols.Freedom: return Outbound.FreedomSettings.fromJson(json);
-            case Protocols.Blackhole: return Outbound.BlackholeSettings.fromJson(json);
-            case Protocols.DNS: return Outbound.DNSSettings.fromJson(json);
-            case Protocols.VMess: return Outbound.VmessSettings.fromJson(json);
-            case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json);
-            case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json);
-            case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);
-            case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
-            case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
-            case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
-            case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);
-            case Protocols.Loopback: return Outbound.LoopbackSettings.fromJson(json);
-            default: return null;
-        }
-    }
-
-    toJson() {
-        return {};
-    }
-};
-Outbound.FreedomSettings = class extends CommonClass {
-    constructor(
-        domainStrategy = '',
-        redirect = '',
-        fragment = {},
-        noises = [],
-        finalRules = [],
-    ) {
-        super();
-        this.domainStrategy = domainStrategy;
-        this.redirect = redirect;
-        this.fragment = fragment || {};
-        this.noises = Array.isArray(noises) ? noises : [];
-        this.finalRules = Array.isArray(finalRules)
-            ? finalRules.map((rule: any) => rule instanceof Outbound.FreedomSettings.FinalRule ? rule : Outbound.FreedomSettings.FinalRule.fromJson(rule))
-            : [];
-    }
-
-    addNoise() {
-        this.noises.push(new Outbound.FreedomSettings.Noise());
-    }
-
-    delNoise(index: number) {
-        this.noises.splice(index, 1);
-    }
-
-    addFinalRule(action = 'block') {
-        this.finalRules.push(new Outbound.FreedomSettings.FinalRule(action));
-    }
-
-    delFinalRule(index: number) {
-        this.finalRules.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}): any {
-        const finalRules = Array.isArray(json.finalRules)
-            ? json.finalRules.map((rule: any) => Outbound.FreedomSettings.FinalRule.fromJson(rule))
-            : [];
-
-        // Backward compatibility: map legacy ipsBlocked entries to blocking finalRules.
-        if (finalRules.length === 0 && Array.isArray(json.ipsBlocked) && json.ipsBlocked.length > 0) {
-            finalRules.push(new Outbound.FreedomSettings.FinalRule('block', '', '', json.ipsBlocked, ''));
-        }
-
-        return new Outbound.FreedomSettings(
-            json.domainStrategy,
-            json.redirect,
-            json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : {},
-            json.noises ? json.noises.map((noise: any) => Outbound.FreedomSettings.Noise.fromJson(noise)) : [],
-            finalRules,
-        );
-    }
-
-    toJson() {
-        return {
-            domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
-            redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,
-            fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
-            noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
-            finalRules: this.finalRules.length === 0 ? undefined : Outbound.FreedomSettings.FinalRule.toJsonArray(this.finalRules),
-        };
-    }
-};
-
-Outbound.FreedomSettings.Fragment = class extends CommonClass {
-    constructor(
-        packets = '1-3',
-        length = '',
-        interval = '',
-        maxSplit = ''
-    ) {
-        super();
-        this.packets = packets;
-        this.length = length;
-        this.interval = interval;
-        this.maxSplit = maxSplit;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.FreedomSettings.Fragment(
-            json.packets,
-            json.length,
-            json.interval,
-            json.maxSplit
-        );
-    }
-};
-
-Outbound.FreedomSettings.Noise = class extends CommonClass {
-    constructor(
-        type = 'rand',
-        packet = '10-20',
-        delay = '10-16',
-        applyTo = 'ip'
-    ) {
-        super();
-        this.type = type;
-        this.packet = packet;
-        this.delay = delay;
-        this.applyTo = applyTo;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.FreedomSettings.Noise(
-            json.type,
-            json.packet,
-            json.delay,
-            json.applyTo
-        );
-    }
-
-    toJson() {
-        return {
-            type: this.type,
-            packet: this.packet,
-            delay: this.delay,
-            applyTo: this.applyTo
-        };
-    }
-};
-
-Outbound.FreedomSettings.FinalRule = class extends CommonClass {
-    constructor(action = 'block', network = '', port = '', ip = [], blockDelay = '') {
-        super();
-        this.action = action;
-        this.network = network;
-        this.port = port;
-        this.ip = Array.isArray(ip) ? ip : [];
-        this.blockDelay = blockDelay;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.FreedomSettings.FinalRule(
-            json.action,
-            Array.isArray(json.network) ? json.network.join(',') : json.network,
-            json.port,
-            json.ip || [],
-            json.blockDelay,
-        );
-    }
-
-    toJson() {
-        return {
-            action: ['allow', 'block'].includes(this.action) ? this.action : 'block',
-            network: ObjectUtil.isEmpty(this.network) ? undefined : this.network,
-            port: ObjectUtil.isEmpty(this.port) ? undefined : this.port,
-            ip: this.ip.length === 0 ? undefined : this.ip,
-            blockDelay: this.action === 'block' && !ObjectUtil.isEmpty(this.blockDelay) ? this.blockDelay : undefined,
-        };
-    }
-};
-
-Outbound.BlackholeSettings = class extends CommonClass {
-    constructor(type?: any) {
-        super();
-        this.type = type;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.BlackholeSettings(
-            json.response ? json.response.type : undefined,
-        );
-    }
-
-    toJson() {
-        return {
-            response: ObjectUtil.isEmpty(this.type) ? undefined : { type: this.type },
-        };
-    }
-};
-
-Outbound.LoopbackSettings = class extends CommonClass {
-    constructor(inboundTag = '') {
-        super();
-        this.inboundTag = inboundTag;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.LoopbackSettings(json.inboundTag || '');
-    }
-
-    toJson() {
-        return {
-            inboundTag: this.inboundTag || undefined,
-        };
-    }
-};
-
-Outbound.DNSRule = class extends CommonClass {
-    constructor(action = 'direct', qtype = '', domain = '') {
-        super();
-        this.action = action;
-        this.qtype = qtype;
-        this.domain = domain;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.DNSRule(
-            json.action,
-            normalizeDNSRuleField(json.qtype),
-            normalizeDNSRuleField(json.domain),
-        );
-    }
-
-    toJson() {
-        const rule: any = {
-            action: normalizeDNSRuleAction(this.action),
-        };
-
-        const qtype = normalizeDNSRuleField(this.qtype);
-        if (!ObjectUtil.isEmpty(qtype)) {
-            if (/^\d+$/.test(qtype)) {
-                rule.qtype = Number(qtype);
-            } else {
-                rule.qtype = qtype;
-            }
-        }
-
-        const domains = normalizeDNSRuleField(this.domain)
-            .split(',')
-            .map(d => d.trim())
-            .filter(d => d.length > 0);
-        if (domains.length > 0) {
-            rule.domain = domains;
-        }
-
-        return rule;
-    }
-};
-
-Outbound.DNSSettings = class extends CommonClass {
-    constructor(
-        rewriteNetwork = '',
-        rewriteAddress = '',
-        rewritePort = 53,
-        userLevel = 0,
-        rules = []
-    ) {
-        super();
-        this.rewriteNetwork = rewriteNetwork;
-        this.rewriteAddress = rewriteAddress;
-        this.rewritePort = rewritePort;
-        this.userLevel = userLevel;
-        this.rules = Array.isArray(rules) ? rules.map((rule: any) => rule instanceof Outbound.DNSRule ? rule : Outbound.DNSRule.fromJson(rule)) : [];
-    }
-
-    addRule(action = 'direct') {
-        this.rules.push(new Outbound.DNSRule(action));
-    }
-
-    delRule(index: number) {
-        this.rules.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}): any {
-        // Spec uses rewrite{Network,Address,Port}; older configs used the
-        // bare network/address/port keys — accept both so existing saved
-        // configs keep working after the migration.
-        return new Outbound.DNSSettings(
-            json.rewriteNetwork ?? json.network ?? '',
-            json.rewriteAddress ?? json.address ?? '',
-            Number(json.rewritePort ?? json.port ?? 53) || 53,
-            Number(json.userLevel ?? 0) || 0,
-            getDNSRulesFromJson(json),
-        );
-    }
-
-    toJson() {
-        const json: any = {};
-        if (!ObjectUtil.isEmpty(this.rewriteNetwork)) json.rewriteNetwork = this.rewriteNetwork;
-        if (!ObjectUtil.isEmpty(this.rewriteAddress)) json.rewriteAddress = this.rewriteAddress;
-        if (this.rewritePort > 0) json.rewritePort = this.rewritePort;
-        if (this.userLevel > 0) json.userLevel = this.userLevel;
-        if (this.rules.length > 0) json.rules = Outbound.DNSRule.toJsonArray(this.rules);
-        return json;
-    }
-};
-Outbound.VmessSettings = class extends CommonClass {
-    constructor(address?: any, port?: any, id?: any, security?: any) {
-        super();
-        this.address = address;
-        this.port = port;
-        this.id = id;
-        this.security = security;
-    }
-
-    static fromJson(json: any = {}): any {
-        if (!ObjectUtil.isArrEmpty(json.vnext)) {
-            const v = json.vnext[0] || {};
-            const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
-            return new Outbound.VmessSettings(
-                v.address,
-                v.port,
-                u.id,
-                u.security,
-            );
-        }
-    }
-
-    toJson() {
-        return {
-            vnext: [{
-                address: this.address,
-                port: this.port,
-                users: [{
-                    id: this.id,
-                    security: this.security
-                }]
-            }]
-        };
-    }
-};
-Outbound.VLESSSettings = class extends CommonClass {
-    constructor(address?: any, port?: any, id?: any, flow?: any, encryption: any = 'none', reverseTag: any = '', reverseSniffing: any = new ReverseSniffing(), testpre: any = 0, testseed: any[] = []) {
-        super();
-        this.address = address;
-        this.port = port;
-        this.id = id;
-        this.flow = flow;
-        this.encryption = encryption || 'none';
-        this.reverseTag = reverseTag;
-        this.reverseSniffing = reverseSniffing;
-        this.testpre = testpre;
-        this.testseed = testseed;
-    }
-
-    static fromJson(json: any = {}): any {
-        // Handle v2rayN-style nested vnext array (standard Xray JSON format)
-        if (!ObjectUtil.isArrEmpty(json.vnext)) {
-            const v = json.vnext[0] || {};
-            const u = ObjectUtil.isArrEmpty(v.users) ? {} : v.users[0];
-            const saved = json.testseed;
-            const testseed = (Array.isArray(saved)
-                && saved.length === 4
-                && saved.every((v: any) => Number.isInteger(v) && v > 0))
-                ? saved
-                : [];
-            return new Outbound.VLESSSettings(
-                v.address,
-                v.port,
-                u.id,
-                u.flow,
-                u.encryption,
-                json.reverse?.tag || '',
-                ReverseSniffing.fromJson(json.reverse?.sniffing || {}),
-                json.testpre || 0,
-                testseed,
-            );
-        }
-        if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
-        const saved = json.testseed;
-        const testseed = (Array.isArray(saved)
-            && saved.length === 4
-            && saved.every((v: any) => Number.isInteger(v) && v > 0))
-            ? saved
-            : [];
-        return new Outbound.VLESSSettings(
-            json.address,
-            json.port,
-            json.id,
-            json.flow,
-            json.encryption,
-            json.reverse?.tag || '',
-            ReverseSniffing.fromJson(json.reverse?.sniffing || {}),
-            json.testpre || 0,
-            testseed,
-        );
-    }
-
-    toJson() {
-        const result: any = {
-            address: this.address,
-            port: this.port,
-            id: this.id,
-            flow: this.flow,
-            encryption: this.encryption || 'none',
-        };
-        if (!ObjectUtil.isEmpty(this.reverseTag)) {
-            const reverseSniffing = this.reverseSniffing ? this.reverseSniffing.toJson() : {};
-            const defaultReverseSniffing = new ReverseSniffing().toJson();
-            result.reverse = {
-                tag: this.reverseTag,
-                sniffing: JSON.stringify(reverseSniffing) === JSON.stringify(defaultReverseSniffing) ? {} : reverseSniffing,
-            };
-        }
-        // Vision-specific knobs are only meaningful for the exact xtls-rprx-vision flow.
-        if (this.flow === TLS_FLOW_CONTROL.VISION) {
-            if (this.testpre > 0) {
-                result.testpre = this.testpre;
-            }
-            if (Array.isArray(this.testseed)
-                && this.testseed.length === 4
-                && this.testseed.every((v: any) => Number.isInteger(v) && v > 0)) {
-                result.testseed = this.testseed;
-            }
-        }
-        return result;
-    }
-};
-Outbound.TrojanSettings = class extends CommonClass {
-    constructor(address?: any, port?: any, password?: any) {
-        super();
-        this.address = address;
-        this.port = port;
-        this.password = password;
-    }
-
-    static fromJson(json: any = {}): any {
-        if (ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings();
-        return new Outbound.TrojanSettings(
-            json.servers[0].address,
-            json.servers[0].port,
-            json.servers[0].password,
-        );
-    }
-
-    toJson() {
-        return {
-            servers: [{
-                address: this.address,
-                port: this.port,
-                password: this.password,
-            }],
-        };
-    }
-};
-Outbound.ShadowsocksSettings = class extends CommonClass {
-    constructor(address?: any, port?: any, password?: any, method?: any, uot?: any, UoTVersion?: any) {
-        super();
-        this.address = address;
-        this.port = port;
-        this.password = password;
-        this.method = method;
-        this.uot = uot;
-        this.UoTVersion = UoTVersion;
-    }
-
-    static fromJson(json: any = {}): any {
-        let servers = json.servers;
-        if (ObjectUtil.isArrEmpty(servers)) servers = [{}];
-        return new Outbound.ShadowsocksSettings(
-            servers[0].address,
-            servers[0].port,
-            servers[0].password,
-            servers[0].method,
-            servers[0].uot,
-            servers[0].UoTVersion,
-        );
-    }
-
-    toJson() {
-        return {
-            servers: [{
-                address: this.address,
-                port: this.port,
-                password: this.password,
-                method: this.method,
-                uot: this.uot,
-                UoTVersion: this.UoTVersion,
-            }],
-        };
-    }
-};
-
-Outbound.SocksSettings = class extends CommonClass {
-    constructor(address?: any, port?: any, user?: any, pass?: any) {
-        super();
-        this.address = address;
-        this.port = port;
-        this.user = user;
-        this.pass = pass;
-    }
-
-    static fromJson(json: any = {}): any {
-        let servers = json.servers;
-        if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];
-        return new Outbound.SocksSettings(
-            servers[0].address,
-            servers[0].port,
-            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
-            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
-        );
-    }
-
-    toJson() {
-        return {
-            servers: [{
-                address: this.address,
-                port: this.port,
-                users: ObjectUtil.isEmpty(this.user) ? [] : [{ user: this.user, pass: this.pass }],
-            }],
-        };
-    }
-};
-Outbound.HttpSettings = class extends CommonClass {
-    constructor(address?: any, port?: any, user?: any, pass?: any) {
-        super();
-        this.address = address;
-        this.port = port;
-        this.user = user;
-        this.pass = pass;
-    }
-
-    static fromJson(json: any = {}): any {
-        let servers = json.servers;
-        if (ObjectUtil.isArrEmpty(servers)) servers = [{ users: [{}] }];
-        return new Outbound.HttpSettings(
-            servers[0].address,
-            servers[0].port,
-            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
-            ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
-        );
-    }
-
-    toJson() {
-        return {
-            servers: [{
-                address: this.address,
-                port: this.port,
-                users: ObjectUtil.isEmpty(this.user) ? [] : [{ user: this.user, pass: this.pass }],
-            }],
-        };
-    }
-};
-
-Outbound.WireguardSettings = class extends CommonClass {
-    constructor(
-        mtu = 1420,
-        secretKey = '',
-        address = [''],
-        workers = 2,
-        domainStrategy = '',
-        reserved = '',
-        peers = [new Outbound.WireguardSettings.Peer()],
-        noKernelTun = false,
-    ) {
-        super();
-        this.mtu = mtu;
-        this.secretKey = secretKey;
-        this.pubKey = secretKey.length > 0 ? Wireguard.generateKeypair(secretKey).publicKey : '';
-        this.address = Array.isArray(address) ? address.join(',') : address;
-        this.workers = workers;
-        this.domainStrategy = domainStrategy;
-        this.reserved = Array.isArray(reserved) ? reserved.join(',') : reserved;
-        this.peers = peers;
-        this.noKernelTun = noKernelTun;
-    }
-
-    addPeer() {
-        this.peers.push(new Outbound.WireguardSettings.Peer());
-    }
-
-    delPeer(index: number) {
-        this.peers.splice(index, 1);
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.WireguardSettings(
-            json.mtu,
-            json.secretKey,
-            json.address,
-            json.workers,
-            json.domainStrategy,
-            json.reserved,
-            json.peers.map((peer: any) => Outbound.WireguardSettings.Peer.fromJson(peer)),
-            json.noKernelTun,
-        );
-    }
-
-    toJson() {
-        return {
-            mtu: this.mtu ?? undefined,
-            secretKey: this.secretKey,
-            address: this.address ? this.address.split(",") : [],
-            workers: this.workers ?? undefined,
-            domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined,
-            reserved: this.reserved ? this.reserved.split(",").map(Number) : undefined,
-            peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),
-            noKernelTun: this.noKernelTun,
-        };
-    }
-};
-
-Outbound.WireguardSettings.Peer = class extends CommonClass {
-    constructor(
-        publicKey = '',
-        psk = '',
-        allowedIPs = ['0.0.0.0/0', '::/0'],
-        endpoint = '',
-        keepAlive = 0
-    ) {
-        super();
-        this.publicKey = publicKey;
-        this.psk = psk;
-        this.allowedIPs = allowedIPs;
-        this.endpoint = endpoint;
-        this.keepAlive = keepAlive;
-    }
-
-    static fromJson(json: any = {}): any {
-        return new Outbound.WireguardSettings.Peer(
-            json.publicKey,
-            json.preSharedKey,
-            json.allowedIPs,
-            json.endpoint,
-            json.keepAlive
-        );
-    }
-
-    toJson() {
-        return {
-            publicKey: this.publicKey,
-            preSharedKey: this.psk.length > 0 ? this.psk : undefined,
-            allowedIPs: this.allowedIPs ? this.allowedIPs : undefined,
-            endpoint: this.endpoint,
-            keepAlive: this.keepAlive ?? undefined,
-        };
-    }
-};
-
-Outbound.HysteriaSettings = class extends CommonClass {
-    constructor(address = '', port = 443, version = 2) {
-        super();
-        this.address = address;
-        this.port = port;
-        this.version = version;
-    }
-
-    static fromJson(json: any = {}): any {
-        if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings();
-        return new Outbound.HysteriaSettings(
-            json.address,
-            json.port,
-            json.version
-        );
-    }
-
-    toJson() {
-        return {
-            address: this.address,
-            port: this.port,
-            version: this.version
-        };
-    }
-};

+ 114 - 28
frontend/src/pages/inbounds/InboundFormModal.tsx

@@ -33,6 +33,10 @@ import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from
 import {
   rawInboundToFormValues,
   formValuesToWirePayload,
+  pruneEmpty,
+  normalizeSniffing,
+  normalizeClients,
+  dropLegacyOptionalEmpties,
 } from '@/lib/xray/inbound-form-adapter';
 import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
 import {
@@ -82,7 +86,7 @@ import type { FormInstance } from 'antd';
 import type { NamePath } from 'antd/es/form/interface';
 
 const { TextArea } = Input;
-import type { DBInbound } from '@/models/dbinbound';
+import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
 import type { NodeRecord } from '@/api/queries/useNodesQuery';
 
 // Pattern A rewrite of InboundFormModal. Built as a sibling file so the
@@ -121,7 +125,12 @@ function AdvancedSliceEditor({
     return JSON.stringify(wrapKey ? { [wrapKey]: inner } : inner, null, 2);
   };
 
-  const watched = Form.useWatch(path, form);
+  // preserve: true so useWatch returns the full subtree from the form
+  // store — without it, useWatch goes through getFieldsValue() which
+  // filters out unregistered fields. Slices like `settings` would lose
+  // their `clients` / `fallbacks` sub-trees because those aren't bound
+  // to any Form.Item.
+  const watched = Form.useWatch(path, { form, preserve: true });
   const lastEmitRef = useRef<string>('');
   const [text, setText] = useState(() => {
     const initial = serialize(form.getFieldValue(path));
@@ -172,24 +181,40 @@ function AdvancedAllEditor({
   form: FormInstance<InboundFormValues>;
   streamEnabled: boolean;
 }) {
-  const wListen = Form.useWatch('listen', form);
-  const wPort = Form.useWatch('port', form);
-  const wProtocol = Form.useWatch('protocol', form);
-  const wTag = Form.useWatch('tag', form);
-  const wSettings = Form.useWatch('settings', form);
-  const wSniffing = Form.useWatch('sniffing', form);
-  const wStream = Form.useWatch('streamSettings', form);
+  // preserve: true — default useWatch returns only registered fields, so
+  // sub-trees we never bound (settings.clients/fallbacks, sniffing
+  // defaults, etc.) wouldn't show up. preserve switches the read to
+  // getFieldsValue(true) which returns the full form store.
+  const wListen = Form.useWatch('listen', { form, preserve: true });
+  const wPort = Form.useWatch('port', { form, preserve: true });
+  const wProtocol = Form.useWatch('protocol', { form, preserve: true });
+  const wTag = Form.useWatch('tag', { form, preserve: true });
+  const wSettings = Form.useWatch('settings', { form, preserve: true });
+  const wSniffing = Form.useWatch('sniffing', { form, preserve: true });
+  const wStream = Form.useWatch('streamSettings', { form, preserve: true });
 
   const serialize = () => {
+    // Apply the same prune/normalize as the wire payload so the JSON
+    // shown here is what the panel actually POSTs (no empty defaults,
+    // disabled sniffing as { enabled: false }, finalmask dropped when
+    // there are no masks).
+    const settingsView = (pruneEmpty(wSettings ?? {}) ?? {}) as Record<string, unknown>;
+    if (typeof wProtocol === 'string' && Array.isArray(settingsView.clients)) {
+      settingsView.clients = normalizeClients(wProtocol, settingsView.clients);
+    }
+    const streamView = streamEnabled
+      ? ((pruneEmpty(wStream ?? {}) ?? {}) as Record<string, unknown>)
+      : undefined;
+    dropLegacyOptionalEmpties(settingsView, streamView);
     const out: Record<string, unknown> = {
       listen: wListen ?? '',
       port: wPort ?? 0,
       protocol: wProtocol ?? '',
       tag: wTag ?? '',
-      settings: wSettings ?? {},
-      sniffing: wSniffing ?? {},
+      settings: settingsView,
+      sniffing: normalizeSniffing(wSniffing as Parameters<typeof normalizeSniffing>[0]),
     };
-    if (streamEnabled) out.streamSettings = wStream ?? {};
+    if (streamView) out.streamSettings = streamView;
     return JSON.stringify(out, null, 2);
   };
 
@@ -368,6 +393,39 @@ export default function InboundFormModal({
     return !!msg?.success;
   };
 
+  // Derive a fallback row's SNI / ALPN / Path / xver from a child
+  // inbound's streamSettings — what the legacy panel auto-filled when an
+  // operator wired a fallback target. SNI/ALPN come straight off the
+  // child's TLS block; path depends on the child's transport (ws/grpc
+  // /httpupgrade carry an explicit path; tcp/kcp/xhttp have no path of
+  // their own). xver stays 0 unless the child explicitly opts in via
+  // PROXY-protocol sockopt.
+  const deriveFallbackDefaults = (childId: number): Partial<FallbackRow> => {
+    const child = (dbInbounds || []).find((ib) => ib.id === childId);
+    if (!child) return {};
+    const stream = coerceInboundJsonField(child.streamSettings);
+    const tls = (stream.tlsSettings as Record<string, unknown> | undefined) ?? {};
+    const network = typeof stream.network === 'string' ? stream.network : '';
+    const sni = typeof tls.serverName === 'string' ? tls.serverName : '';
+    const alpnArr = Array.isArray(tls.alpn) ? tls.alpn : [];
+    const alpn = alpnArr.filter((v) => typeof v === 'string').join(',');
+    let path = '';
+    if (network === 'ws') {
+      const ws = (stream.wsSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof ws.path === 'string') path = ws.path;
+    } else if (network === 'grpc') {
+      const grpc = (stream.grpcSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof grpc.serviceName === 'string') path = grpc.serviceName;
+    } else if (network === 'httpupgrade') {
+      const hu = (stream.httpupgradeSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof hu.path === 'string') path = hu.path;
+    } else if (network === 'xhttp') {
+      const xh = (stream.xhttpSettings as Record<string, unknown> | undefined) ?? {};
+      if (typeof xh.path === 'string') path = xh.path;
+    }
+    return { name: sni, alpn, path, xver: 0 };
+  };
+
   const addFallback = () => {
     setFallbacks((prev) => [...prev, {
       rowKey: `fb-${++fallbackKeyRef.current}`,
@@ -380,7 +438,18 @@ export default function InboundFormModal({
   };
 
   const updateFallback = (rowKey: string, patch: Partial<FallbackRow>) => {
-    setFallbacks((prev) => prev.map((r) => r.rowKey === rowKey ? { ...r, ...patch } : r));
+    setFallbacks((prev) => prev.map((r) => {
+      if (r.rowKey !== rowKey) return r;
+      // When the picker selects a new child inbound and the row hasn't
+      // been hand-edited yet (sni/alpn/path all blank, xver = 0), pull
+      // the SNI/ALPN/Path defaults off that child. Operators who
+      // intentionally typed values keep them — we only fill the empties.
+      if (typeof patch.childId === 'number' && patch.childId !== r.childId) {
+        const isPristine = !r.name && !r.alpn && !r.path && r.xver === 0;
+        if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) };
+      }
+      return { ...r, ...patch };
+    }));
   };
 
   const removeFallback = (idx: number) => {
@@ -409,14 +478,17 @@ export default function InboundFormModal({
       const alreadyHave = new Set(prev.map((r) => r.childId));
       const additions = fallbackChildOptions
         .filter((opt) => !alreadyHave.has(opt.value))
-        .map<FallbackRow>((opt) => ({
-          rowKey: `fb-${++fallbackKeyRef.current}`,
-          childId: opt.value,
-          name: '',
-          alpn: '',
-          path: '',
-          xver: 0,
-        }));
+        .map<FallbackRow>((opt) => {
+          const derived = deriveFallbackDefaults(opt.value);
+          return {
+            rowKey: `fb-${++fallbackKeyRef.current}`,
+            childId: opt.value,
+            name: derived.name ?? '',
+            alpn: derived.alpn ?? '',
+            path: derived.path ?? '',
+            xver: derived.xver ?? 0,
+          };
+        });
       if (additions.length === 0) return prev;
       return [...prev, ...additions];
     });
@@ -697,20 +769,34 @@ export default function InboundFormModal({
   };
 
   const submit = async () => {
-    let values: InboundFormValues;
     try {
-      values = await form.validateFields();
+      await form.validateFields();
     } catch {
       return;
     }
+    // Why getFieldsValue(true) instead of the validateFields return value:
+    // rc-component/form's validateFields filters its output by REGISTERED
+    // name paths. settings.clients and settings.fallbacks have no Form.Item
+    // bound to them (clients are managed via the standalone Client modal,
+    // not inside this inbound modal) — so validateFields would drop them
+    // and the update wire payload would silently delete every client on
+    // every save. getFieldsValue(true) returns the entire form store and
+    // keeps those sub-trees intact.
+    const values = form.getFieldsValue(true) as InboundFormValues;
     const parsed = InboundFormSchema.safeParse(values);
     if (!parsed.success) {
       const issue = parsed.error.issues[0];
-      messageApi.error(
-        t(issue?.message ?? 'somethingWentWrong', {
-          defaultValue: issue?.message ?? 'invalid',
-        }),
-      );
+      const path = Array.isArray(issue?.path) && issue.path.length > 0
+        ? issue.path.join('.')
+        : '';
+      const baseMsg = issue?.message ?? 'somethingWentWrong';
+      const display = path ? `${path}: ${baseMsg}` : baseMsg;
+      messageApi.error(t(baseMsg, { defaultValue: display }));
+      console.error('[InboundFormModal] schema validation failed', {
+        path: issue?.path,
+        message: issue?.message,
+        values,
+      });
       return;
     }
     setSaving(true);

+ 199 - 35
frontend/src/pages/inbounds/InboundInfoModal.tsx

@@ -15,9 +15,93 @@ import {
 import { Protocols } from '@/schemas/primitives';
 import InfinityIcon from '@/components/InfinityIcon';
 import { useDatepicker } from '@/hooks/useDatepicker';
+import { coerceInboundJsonField } from '@/models/dbinbound';
+import {
+  canEnableTlsFlow,
+  isSS2022 as isSS2022Helper,
+  isSSMultiUser as isSSMultiUserHelper,
+} from '@/lib/xray/protocol-capabilities';
+import {
+  genAllLinks,
+  genWireguardConfigs,
+  genWireguardLinks,
+} from '@/lib/xray/inbound-link';
+import { inboundFromDb } from '@/lib/xray/inbound-from-db';
 import type { SubSettings } from './useInbounds';
 import './InboundInfoModal.css';
 
+const LINK_PROTOCOLS: ReadonlySet<string> = new Set([
+  Protocols.VMESS,
+  Protocols.VLESS,
+  Protocols.TROJAN,
+  Protocols.SHADOWSOCKS,
+  Protocols.HYSTERIA,
+]);
+
+function hasShareLink(protocol: string): boolean {
+  return LINK_PROTOCOLS.has(protocol);
+}
+
+function readHeader(headers: unknown, name: string): string {
+  const needle = name.toLowerCase();
+  if (Array.isArray(headers)) {
+    for (const h of headers) {
+      if (h && typeof h === 'object' && String((h as { name?: string }).name ?? '').toLowerCase() === needle) {
+        return String((h as { value?: unknown }).value ?? '');
+      }
+    }
+    return '';
+  }
+  if (headers && typeof headers === 'object') {
+    for (const [k, v] of Object.entries(headers as Record<string, unknown>)) {
+      if (k.toLowerCase() === needle) {
+        return Array.isArray(v) ? String(v[0] ?? '') : String(v ?? '');
+      }
+    }
+  }
+  return '';
+}
+
+function readNetworkHost(stream: Record<string, unknown>, network: string): string | null {
+  switch (network) {
+    case 'tcp': {
+      const tcp = stream.tcpSettings as { header?: { request?: { headers?: unknown } } } | undefined;
+      return readHeader(tcp?.header?.request?.headers, 'host');
+    }
+    case 'ws': {
+      const ws = stream.wsSettings as { host?: string; headers?: unknown } | undefined;
+      return (ws?.host && ws.host.length > 0) ? ws.host : readHeader(ws?.headers, 'host');
+    }
+    case 'httpupgrade': {
+      const hu = stream.httpupgradeSettings as { host?: string; headers?: unknown } | undefined;
+      return (hu?.host && hu.host.length > 0) ? hu.host : readHeader(hu?.headers, 'host');
+    }
+    case 'xhttp': {
+      const xh = stream.xhttpSettings as { host?: string; headers?: unknown } | undefined;
+      return (xh?.host && xh.host.length > 0) ? xh.host : readHeader(xh?.headers, 'host');
+    }
+    default:
+      return null;
+  }
+}
+
+function readNetworkPath(stream: Record<string, unknown>, network: string): string | null {
+  switch (network) {
+    case 'tcp': {
+      const tcp = stream.tcpSettings as { header?: { request?: { path?: string[] } } } | undefined;
+      return tcp?.header?.request?.path?.[0] ?? null;
+    }
+    case 'ws':
+      return (stream.wsSettings as { path?: string } | undefined)?.path ?? null;
+    case 'httpupgrade':
+      return (stream.httpupgradeSettings as { path?: string } | undefined)?.path ?? null;
+    case 'xhttp':
+      return (stream.xhttpSettings as { path?: string } | undefined)?.path ?? null;
+    default:
+      return null;
+  }
+}
+
 interface ClientStats {
   email: string;
   up: number;
@@ -44,37 +128,35 @@ interface ClientSetting {
   updated_at?: number;
 }
 
-interface InboundLike {
+interface InboundInfo {
   protocol: string;
-  clients?: ClientSetting[];
-  settings?: Record<string, unknown>;
-  serverName?: string;
-  isTcp?: boolean;
-  isWs?: boolean;
-  isHttpupgrade?: boolean;
-  isXHTTP?: boolean;
-  isGrpc?: boolean;
-  isSSMultiUser?: boolean;
-  isSS2022?: boolean;
-  host?: string;
-  path?: string;
-  serviceName?: string;
-  stream?: {
-    network?: string;
-    security?: string;
+  clients: ClientSetting[];
+  settings: Record<string, unknown>;
+  isTcp: boolean;
+  isWs: boolean;
+  isHttpupgrade: boolean;
+  isXHTTP: boolean;
+  isGrpc: boolean;
+  isSSMultiUser: boolean;
+  isSS2022: boolean;
+  isVlessTlsFlow: boolean;
+  host: string | null;
+  path: string | null;
+  serviceName: string;
+  serverName: string;
+  stream: {
+    network: string;
+    security: string;
     xhttp?: { mode?: string };
     grpc?: { multiMode?: boolean };
   };
-  canEnableTlsFlow?: () => boolean;
-  genWireguardConfigs: (remark: string, model: string, host: string) => string;
-  genWireguardLinks: (remark: string, model: string, host: string) => string;
-  genAllLinks: (remark: string, model: string, client: ClientSetting | null, host: string) => { remark?: string; link: string }[];
 }
 
 interface DBInboundLike {
   id: number;
   address: string;
   port: number;
+  listen: string;
   protocol: string;
   remark: string;
   enable?: boolean;
@@ -85,9 +167,64 @@ interface DBInboundLike {
   isMixed?: boolean;
   isHTTP?: boolean;
   isWireguard?: boolean;
+  settings: unknown;
+  streamSettings: unknown;
+  sniffing: unknown;
   clientStats?: ClientStats[];
-  hasLink: () => boolean;
-  toInbound: () => InboundLike;
+}
+
+function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
+  const settings = coerceInboundJsonField(dbInbound.settings) as Record<string, unknown>;
+  const stream = coerceInboundJsonField(dbInbound.streamSettings) as Record<string, unknown>;
+  const network = (stream.network as string | undefined) ?? '';
+  const security = (stream.security as string | undefined) ?? 'none';
+  const clients = Array.isArray(settings.clients) ? (settings.clients as ClientSetting[]) : [];
+  const xhttpSettings = stream.xhttpSettings as { mode?: string } | undefined;
+  const grpcSettings = stream.grpcSettings as { multiMode?: boolean; serviceName?: string } | undefined;
+  let serverName = '';
+  if (security === 'tls') {
+    const tls = stream.tlsSettings as { sni?: string; serverName?: string } | undefined;
+    serverName = tls?.sni ?? tls?.serverName ?? '';
+  } else if (security === 'reality') {
+    const reality = stream.realitySettings as { serverNames?: string[]; serverName?: string } | undefined;
+    if (Array.isArray(reality?.serverNames)) {
+      serverName = reality.serverNames.join(', ');
+    } else if (reality?.serverName) {
+      serverName = reality.serverName;
+    }
+  }
+  return {
+    protocol: dbInbound.protocol,
+    clients,
+    settings,
+    isTcp: network === 'tcp',
+    isWs: network === 'ws',
+    isHttpupgrade: network === 'httpupgrade',
+    isXHTTP: network === 'xhttp',
+    isGrpc: network === 'grpc',
+    isSSMultiUser: isSSMultiUserHelper({
+      protocol: dbInbound.protocol,
+      settings: settings as { method?: string },
+    }),
+    isSS2022: isSS2022Helper({
+      protocol: dbInbound.protocol,
+      settings: settings as { method?: string },
+    }),
+    isVlessTlsFlow: canEnableTlsFlow({
+      protocol: dbInbound.protocol,
+      streamSettings: { network, security },
+    }),
+    host: readNetworkHost(stream, network),
+    path: readNetworkPath(stream, network),
+    serviceName: grpcSettings?.serviceName ?? '',
+    serverName,
+    stream: {
+      network,
+      security,
+      xhttp: xhttpSettings ? { mode: xhttpSettings.mode } : undefined,
+      grpc: grpcSettings ? { multiMode: grpcSettings.multiMode } : undefined,
+    },
+  };
 }
 
 interface InboundInfoModalProps {
@@ -155,7 +292,7 @@ export default function InboundInfoModal({
   const { t } = useTranslation();
   const { datepicker } = useDatepicker();
 
-  const [inbound, setInbound] = useState<InboundLike | null>(null);
+  const [inbound, setInbound] = useState<InboundInfo | null>(null);
   const [clientSettings, setClientSettings] = useState<ClientSetting | null>(null);
   const [clientStats, setClientStats] = useState<ClientStats | null>(null);
   const [links, setLinks] = useState<{ remark?: string; link: string }[]>([]);
@@ -213,24 +350,51 @@ export default function InboundInfoModal({
 
   useEffect(() => {
     if (!open || !dbInbound) return;
-    const parsed = dbInbound.toInbound();
-    setInbound(parsed);
-    setActiveTab((parsed.clients?.length ?? 0) > 0 ? 'client' : 'inbound');
+    const info = buildInboundInfo(dbInbound);
+    setInbound(info);
+    setActiveTab(info.clients.length > 0 ? 'client' : 'inbound');
 
     const idx = clientIndex ?? 0;
-    const clientSet = (parsed.clients?.length ?? 0) > 0 ? (parsed.clients?.[idx] || null) : null;
+    const clientSet = info.clients.length > 0 ? (info.clients[idx] || null) : null;
     setClientSettings(clientSet);
     const stats = clientSet
       ? (dbInbound.clientStats || []).find((s) => s.email === clientSet.email) || null
       : null;
     setClientStats(stats);
 
-    if (parsed.protocol === Protocols.WIREGUARD) {
-      setWireguardConfigs(parsed.genWireguardConfigs(dbInbound.remark, '-ieo', nodeAddress).split('\r\n'));
-      setWireguardLinks(parsed.genWireguardLinks(dbInbound.remark, '-ieo', nodeAddress).split('\r\n'));
+    const inboundForLinks = inboundFromDb(dbInbound);
+    const fallbackHostname = window.location.hostname;
+    if (info.protocol === Protocols.WIREGUARD) {
+      setWireguardConfigs(
+        genWireguardConfigs({
+          inbound: inboundForLinks,
+          remark: dbInbound.remark,
+          remarkModel: '-ieo',
+          hostOverride: nodeAddress,
+          fallbackHostname,
+        }).split('\r\n'),
+      );
+      setWireguardLinks(
+        genWireguardLinks({
+          inbound: inboundForLinks,
+          remark: dbInbound.remark,
+          remarkModel: '-ieo',
+          hostOverride: nodeAddress,
+          fallbackHostname,
+        }).split('\r\n'),
+      );
       setLinks([]);
     } else {
-      setLinks(parsed.genAllLinks(dbInbound.remark, remarkModel, clientSet, nodeAddress));
+      setLinks(
+        genAllLinks({
+          inbound: inboundForLinks,
+          remark: dbInbound.remark,
+          remarkModel,
+          client: (clientSet ?? {}) as Parameters<typeof genAllLinks>[0]['client'],
+          hostOverride: nodeAddress,
+          fallbackHostname,
+        }),
+      );
       setWireguardConfigs([]);
       setWireguardLinks([]);
     }
@@ -340,7 +504,7 @@ export default function InboundInfoModal({
           {dbInbound.isVMess && (
             <tr><td>{t('security')}</td><td><Tag>{clientSettings?.security}</Tag></td></tr>
           )}
-          {inbound.canEnableTlsFlow?.() && (
+          {inbound.isVlessTlsFlow && (
             <tr>
               <td>Flow</td>
               <td>
@@ -484,7 +648,7 @@ export default function InboundInfoModal({
         </>
       )}
 
-      {dbInbound.hasLink() && links.length > 0 && (
+      {hasShareLink(dbInbound.protocol) && links.length > 0 && (
         <>
           <Divider>{t('pages.inbounds.copyLink')}</Divider>
           {links.map((link, idx) => (
@@ -584,7 +748,7 @@ export default function InboundInfoModal({
           </>
         )}
 
-        {dbInbound.hasLink() && (
+        {hasShareLink(dbInbound.protocol) && (
           <>
             <div className="info-row">
               <dt>{t('security')}</dt>

+ 55 - 24
frontend/src/pages/inbounds/InboundList.tsx

@@ -34,8 +34,43 @@ import { HttpUtil, SizeFormatter, IntlUtil, ColorUtils } from '@/utils';
 import InfinityIcon from '@/components/InfinityIcon';
 import { useDatepicker } from '@/hooks/useDatepicker';
 import type { NodeRecord } from '@/api/queries/useNodesQuery';
+import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
+import { coerceInboundJsonField } from '@/models/dbinbound';
 import './InboundList.css';
 
+interface StreamHints {
+  network: string;
+  isTls: boolean;
+  isReality: boolean;
+}
+
+function readStreamHints(streamSettings: unknown): StreamHints {
+  const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
+  return {
+    network: stream.network ?? '',
+    isTls: stream.security === 'tls',
+    isReality: stream.security === 'reality',
+  };
+}
+
+function readSettings(settings: unknown): { method?: string } {
+  return coerceInboundJsonField(settings) as { method?: string };
+}
+
+function isInboundMultiUser(record: { protocol: string; settings: unknown }): boolean {
+  switch (record.protocol) {
+    case 'vmess':
+    case 'vless':
+    case 'trojan':
+    case 'hysteria':
+      return true;
+    case 'shadowsocks':
+      return isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(record.settings) });
+    default:
+      return false;
+  }
+}
+
 type ProtocolFlags = {
   isVMess?: boolean;
   isVLess?: boolean;
@@ -59,11 +94,8 @@ interface DBInboundRecord extends ProtocolFlags {
   expiryTime: number;
   _expiryTime: { valueOf(): number } | null;
   nodeId?: number | null;
-  toInbound: () => {
-    stream?: { network?: string; isTls?: boolean; isReality?: boolean };
-    isSSMultiUser?: boolean;
-  };
-  isMultiUser: () => boolean;
+  settings: unknown;
+  streamSettings: unknown;
 }
 
 export interface ClientCountEntry {
@@ -137,11 +169,7 @@ const SORT_FNS: Record<SortKey, (a: DBInboundRecord, b: DBInboundRecord, ctx: {
 function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
   if (dbInbound.isWireguard) return true;
   if (dbInbound.isSS) {
-    try {
-      return !dbInbound.toInbound().isSSMultiUser;
-    } catch {
-      return false;
-    }
+    return !isSSMultiUser({ protocol: 'shadowsocks', settings: readSettings(dbInbound.settings) });
   }
   return false;
 }
@@ -161,7 +189,7 @@ function buildRowActionsMenu({ record, subEnable, t, isMobile }: { record: DBInb
   if (showQrCodeMenu(record)) {
     items.push({ key: 'qrcode', icon: <QrcodeOutlined />, label: t('qrCode') });
   }
-  if (record.isMultiUser()) {
+  if (isInboundMultiUser(record)) {
     items.push({ key: 'export', icon: <ExportOutlined />, label: t('pages.inbounds.export') });
     if (subEnable) {
       items.push({
@@ -341,14 +369,14 @@ export default function InboundList({
         render: (_, record) => {
           const tags: ReactElement[] = [<Tag key="p" color="purple">{record.protocol}</Tag>];
           if (record.isVMess || record.isVLess || record.isTrojan || record.isSS || record.isHysteria) {
-            const stream = record.toInbound().stream;
+            const stream = readStreamHints(record.streamSettings);
             tags.push(
               <Tag key="n" color="green">
-                {record.isHysteria ? 'UDP' : stream?.network}
+                {record.isHysteria ? 'UDP' : stream.network}
               </Tag>,
             );
-            if (stream?.isTls) tags.push(<Tag key="tls" color="blue">TLS</Tag>);
-            if (stream?.isReality) tags.push(<Tag key="reality" color="blue">Reality</Tag>);
+            if (stream.isTls) tags.push(<Tag key="tls" color="blue">TLS</Tag>);
+            if (stream.isReality) tags.push(<Tag key="reality" color="blue">Reality</Tag>);
           }
           return <div className="protocol-tags">{tags}</div>;
         },
@@ -578,15 +606,18 @@ export default function InboundList({
             <div className="stat-row">
               <span className="stat-label">{t('pages.inbounds.protocol')}</span>
               <Tag color="purple">{statsRecord.protocol}</Tag>
-              {(statsRecord.isVMess || statsRecord.isVLess || statsRecord.isTrojan || statsRecord.isSS || statsRecord.isHysteria) && (
-                <>
-                  <Tag color="green">
-                    {statsRecord.isHysteria ? 'UDP' : statsRecord.toInbound().stream?.network}
-                  </Tag>
-                  {statsRecord.toInbound().stream?.isTls && <Tag color="blue">TLS</Tag>}
-                  {statsRecord.toInbound().stream?.isReality && <Tag color="blue">Reality</Tag>}
-                </>
-              )}
+              {(statsRecord.isVMess || statsRecord.isVLess || statsRecord.isTrojan || statsRecord.isSS || statsRecord.isHysteria) && (() => {
+                const stream = readStreamHints(statsRecord.streamSettings);
+                return (
+                  <>
+                    <Tag color="green">
+                      {statsRecord.isHysteria ? 'UDP' : stream.network}
+                    </Tag>
+                    {stream.isTls && <Tag color="blue">TLS</Tag>}
+                    {stream.isReality && <Tag color="blue">Reality</Tag>}
+                  </>
+                );
+              })()}
             </div>
             <div className="stat-row">
               <span className="stat-label">{t('pages.inbounds.port')}</span>

+ 45 - 25
frontend/src/pages/inbounds/InboundsPage.tsx

@@ -21,6 +21,8 @@ import {
 
 import { HttpUtil, SizeFormatter, RandomUtil } from '@/utils';
 import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
+import { genInboundLinks } from '@/lib/xray/inbound-link';
+import { inboundFromDb } from '@/lib/xray/inbound-from-db';
 import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
 import { useTheme } from '@/hooks/useTheme';
 import { useMediaQuery } from '@/hooks/useMediaQuery';
@@ -179,13 +181,13 @@ export default function InboundsPage() {
     const projected = JSON.parse(JSON.stringify(child)) as DBInbound;
     projected.listen = master.listen;
     projected.port = master.port;
-    const masterStream = master.toInbound().stream;
-    const childInbound = child.toInbound();
-    childInbound.stream.security = masterStream.security;
-    childInbound.stream.tls = masterStream.tls;
-    childInbound.stream.reality = masterStream.reality;
-    childInbound.stream.externalProxy = masterStream.externalProxy;
-    projected.streamSettings = childInbound.stream.toString();
+    const masterStream = coerceInboundJsonField(master.streamSettings) as Record<string, unknown>;
+    const childStream = { ...(coerceInboundJsonField(child.streamSettings) as Record<string, unknown>) };
+    childStream.security = masterStream.security;
+    childStream.tlsSettings = masterStream.tlsSettings;
+    childStream.realitySettings = masterStream.realitySettings;
+    childStream.externalProxy = masterStream.externalProxy;
+    projected.streamSettings = JSON.stringify(childStream);
     const Ctor = child.constructor as new (data: DBInbound) => DBInbound;
     return new Ctor(projected);
   }, []);
@@ -199,11 +201,12 @@ export default function InboundsPage() {
     if (!dbInbound?.listen?.startsWith?.('@')) return dbInbound;
     for (const candidate of dbInbounds) {
       if (candidate.id === dbInbound.id) continue;
-      const parsed = candidate.toInbound();
-      if (!parsed.isTcp) continue;
-      if (!['trojan', 'vless'].includes(parsed.protocol)) continue;
-      const fallbacks = parsed.settings.fallbacks || [];
-      if (!fallbacks.find((f: { dest?: string }) => f.dest === dbInbound.listen)) continue;
+      if (!['trojan', 'vless'].includes(candidate.protocol)) continue;
+      const candStream = coerceInboundJsonField(candidate.streamSettings) as { network?: string };
+      if (candStream.network !== 'tcp') continue;
+      const candSettings = coerceInboundJsonField(candidate.settings) as { fallbacks?: { dest?: string }[] };
+      const fallbacks = candSettings.fallbacks || [];
+      if (!fallbacks.find((f) => f.dest === dbInbound.listen)) continue;
       return projectChildThroughMaster(dbInbound, candidate);
     }
     return dbInbound;
@@ -211,8 +214,8 @@ export default function InboundsPage() {
 
   const findClientIndex = useCallback((dbInbound: DBInbound, client: ClientMatchTarget | null) => {
     if (!client) return 0;
-    const inbound = dbInbound.toInbound();
-    const clients = (inbound?.clients || []) as ClientMatchTarget[];
+    const settings = coerceInboundJsonField(dbInbound.settings) as { clients?: ClientMatchTarget[] };
+    const clients = settings.clients || [];
     const idx = clients.findIndex((c) => {
       if (!c) return false;
       switch (dbInbound.protocol) {
@@ -230,7 +233,13 @@ export default function InboundsPage() {
     const projected = checkFallback(dbInbound);
     openText({
       title: t('pages.inbounds.exportLinksTitle'),
-      content: projected.genInboundLinks(remarkModel, hostOverrideFor(dbInbound)),
+      content: genInboundLinks({
+        inbound: inboundFromDb(projected),
+        remark: projected.remark,
+        remarkModel,
+        hostOverride: hostOverrideFor(dbInbound),
+        fallbackHostname: window.location.hostname,
+      }),
       fileName: projected.remark || 'inbound',
     });
   }, [checkFallback, remarkModel, hostOverrideFor, openText, t]);
@@ -240,8 +249,8 @@ export default function InboundsPage() {
   }, [openText, t]);
 
   const exportInboundSubs = useCallback((dbInbound: DBInbound) => {
-    const inbound = dbInbound.toInbound();
-    const clients = (inbound?.clients || []) as { subId?: string }[];
+    const settings = coerceInboundJsonField(dbInbound.settings) as { clients?: { subId?: string }[] };
+    const clients = settings.clients || [];
     const subLinks: string[] = [];
     for (const c of clients) {
       if (c.subId && subSettings.subURI) {
@@ -262,7 +271,13 @@ export default function InboundsPage() {
     const out: string[] = [];
     for (const ib of hydrated) {
       const projected = checkFallback(ib);
-      out.push(projected.genInboundLinks(remarkModel, hostOverrideFor(ib)));
+      out.push(genInboundLinks({
+        inbound: inboundFromDb(projected),
+        remark: projected.remark,
+        remarkModel,
+        hostOverride: hostOverrideFor(ib),
+        fallbackHostname: window.location.hostname,
+      }));
     }
     openText({ title: t('pages.inbounds.exportAllLinksTitle'), content: out.join('\r\n'), fileName: 'All-Inbounds' });
   }, [dbInbounds, hydrateInbound, checkFallback, remarkModel, hostOverrideFor, openText, t]);
@@ -273,8 +288,8 @@ export default function InboundsPage() {
     );
     const out: string[] = [];
     for (const ib of hydrated) {
-      const inbound = ib.toInbound();
-      const clients = (inbound?.clients || []) as { subId?: string }[];
+      const settings = coerceInboundJsonField(ib.settings) as { clients?: { subId?: string }[] };
+      const clients = settings.clients || [];
       for (const c of clients) {
         if (c.subId && subSettings.subURI) {
           out.push(subSettings.subURI + c.subId);
@@ -347,16 +362,21 @@ export default function InboundsPage() {
       okText: t('pages.inbounds.clone'),
       cancelText: t('cancel'),
       onOk: async () => {
-        const baseInbound = dbInbound.toInbound();
         let clonedSettings: string;
         try {
           const raw = coerceInboundJsonField(dbInbound.settings);
           raw.clients = [];
           clonedSettings = JSON.stringify(raw);
         } catch {
-          const fallback = createDefaultInboundSettings(baseInbound.protocol);
+          const fallback = createDefaultInboundSettings(dbInbound.protocol);
           clonedSettings = fallback ? JSON.stringify(fallback, null, 2) : '{}';
         }
+        const streamSettingsString = typeof dbInbound.streamSettings === 'string'
+          ? dbInbound.streamSettings
+          : JSON.stringify(dbInbound.streamSettings ?? {});
+        const sniffingString = typeof dbInbound.sniffing === 'string'
+          ? dbInbound.sniffing
+          : JSON.stringify(dbInbound.sniffing ?? {});
         const data = {
           up: 0,
           down: 0,
@@ -366,10 +386,10 @@ export default function InboundsPage() {
           expiryTime: 0,
           listen: '',
           port: RandomUtil.randomInteger(10000, 60000),
-          protocol: baseInbound.protocol,
+          protocol: dbInbound.protocol,
           settings: clonedSettings,
-          streamSettings: baseInbound.stream.toString(),
-          sniffing: baseInbound.sniffing.toString(),
+          streamSettings: streamSettingsString,
+          sniffing: sniffingString,
         };
         const msg = await HttpUtil.post('/panel/api/inbounds/add', data);
         if (msg?.success) await refresh();

+ 37 - 17
frontend/src/pages/inbounds/QrCodeModal.tsx

@@ -4,6 +4,12 @@ import { Collapse, Modal } from 'antd';
 import type { CollapseProps } from 'antd';
 
 import { Protocols } from '@/schemas/primitives';
+import {
+  genAllLinks,
+  genWireguardConfigs,
+  genWireguardLinks,
+} from '@/lib/xray/inbound-link';
+import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
 import QrPanel from './QrPanel';
 import type { SubSettings } from './useInbounds';
 
@@ -13,22 +19,10 @@ interface ClientSetting {
   [k: string]: unknown;
 }
 
-interface DBInboundLike {
-  remark?: string;
-  toInbound: () => InboundLike;
-}
-
-interface InboundLike {
-  protocol: string;
-  genWireguardConfigs: (remark: string, model: string, host: string) => string;
-  genWireguardLinks: (remark: string, model: string, host: string) => string;
-  genAllLinks: (remark: string, model: string, client: ClientSetting | null, host: string) => { remark?: string; link: string }[];
-}
-
 interface QrCodeModalProps {
   open: boolean;
   onClose: () => void;
-  dbInbound: DBInboundLike | null;
+  dbInbound: (DbInboundLike & { remark?: string }) | null;
   client?: ClientSetting | null;
   remarkModel?: string;
   nodeAddress?: string;
@@ -61,16 +55,42 @@ export default function QrCodeModal({
 
   useEffect(() => {
     if (!open || !dbInbound) return;
-    const inbound = dbInbound.toInbound();
+    const inbound = inboundFromDb(dbInbound);
+    const fallbackHostname = window.location.hostname;
     if (inbound.protocol === Protocols.WIREGUARD) {
       const peerRemark = client?.email
         ? `${dbInbound.remark}-${client.email}`
         : dbInbound.remark || '';
-      setWireguardConfigs(inbound.genWireguardConfigs(peerRemark, '-ieo', nodeAddress).split('\r\n'));
-      setWireguardLinks(inbound.genWireguardLinks(peerRemark, '-ieo', nodeAddress).split('\r\n'));
+      setWireguardConfigs(
+        genWireguardConfigs({
+          inbound,
+          remark: peerRemark,
+          remarkModel: '-ieo',
+          hostOverride: nodeAddress,
+          fallbackHostname,
+        }).split('\r\n'),
+      );
+      setWireguardLinks(
+        genWireguardLinks({
+          inbound,
+          remark: peerRemark,
+          remarkModel: '-ieo',
+          hostOverride: nodeAddress,
+          fallbackHostname,
+        }).split('\r\n'),
+      );
       setLinks([]);
     } else {
-      setLinks(inbound.genAllLinks(dbInbound.remark || '', remarkModel, client, nodeAddress) as { remark?: string; link: string }[]);
+      setLinks(
+        genAllLinks({
+          inbound,
+          remark: dbInbound.remark || '',
+          remarkModel,
+          client: client ?? {},
+          hostOverride: nodeAddress,
+          fallbackHostname,
+        }),
+      );
       setWireguardConfigs([]);
       setWireguardLinks([]);
     }

+ 20 - 10
frontend/src/pages/inbounds/useInbounds.ts

@@ -3,8 +3,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
 
 import { HttpUtil } from '@/utils';
 import { parseMsg } from '@/utils/zodValidate';
-import { DBInbound } from '@/models/dbinbound';
+import { DBInbound, coerceInboundJsonField } from '@/models/dbinbound';
 import { Protocols } from '@/schemas/primitives';
+import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
 import { setDatepicker } from '@/hooks/useDatepicker';
 import { keys } from '@/api/queryKeys';
 import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
@@ -201,12 +202,14 @@ export function useInbounds() {
   const rebuildClientCount = useCallback(() => {
     const counts: Record<number, ClientRollup> = {};
     for (const dbInbound of dbInboundsRef.current) {
-      const parsed = (dbInbound as unknown as { toInbound: () => { clients?: unknown[]; isSSMultiUser?: boolean }; isSS: boolean; protocol: string }).toInbound();
-      const protocol = (dbInbound as unknown as { protocol: string }).protocol;
+      const protocol = dbInbound.protocol;
       if (!TRACKED_PROTOCOLS.includes(protocol)) continue;
-      const isSS = (dbInbound as unknown as { isSS: boolean }).isSS;
-      if (isSS && !parsed.isSSMultiUser) continue;
-      counts[(dbInbound as unknown as { id: number }).id] = rollupClients(dbInbound, parsed as { clients?: { email?: string; enable?: boolean; comment?: string }[] });
+      const settings = coerceInboundJsonField(dbInbound.settings) as {
+        method?: string;
+        clients?: Array<{ email?: string; enable?: boolean; comment?: string }>;
+      };
+      if (protocol === Protocols.SHADOWSOCKS && !isSSMultiUser({ protocol, settings })) continue;
+      counts[dbInbound.id] = rollupClients(dbInbound, { clients: settings.clients });
     }
     setClientCount(counts);
   }, [rollupClients]);
@@ -219,11 +222,14 @@ export function useInbounds() {
     const counts: Record<number, ClientRollup> = {};
     for (const row of slimQuery.data as { protocol: string; id: number }[]) {
       const dbInbound = new DBInbound(row) as DBInboundInstance;
-      const parsed = (dbInbound as unknown as { toInbound: () => { clients?: unknown[]; isSSMultiUser?: boolean } }).toInbound();
       next.push(dbInbound);
       if (TRACKED_PROTOCOLS.includes(row.protocol)) {
-        if ((dbInbound as unknown as { isSS: boolean }).isSS && !parsed.isSSMultiUser) continue;
-        counts[row.id] = rollupClients(dbInbound, parsed as { clients?: { email?: string; enable?: boolean; comment?: string }[] });
+        const settings = coerceInboundJsonField(dbInbound.settings) as {
+          method?: string;
+          clients?: Array<{ email?: string; enable?: boolean; comment?: string }>;
+        };
+        if (row.protocol === Protocols.SHADOWSOCKS && !isSSMultiUser({ protocol: row.protocol, settings })) continue;
+        counts[row.id] = rollupClients(dbInbound, { clients: settings.clients });
       }
     }
     dbInboundsRef.current = next;
@@ -245,8 +251,12 @@ export function useInbounds() {
   const fetched = slimQuery.data !== undefined && defaultsQuery.data !== undefined;
 
   const refresh = useCallback(async () => {
+    // Invalidate at the inbounds root so both `slim` (this page's list)
+    // and `options` (the Clients page's inbound picker) refetch. Without
+    // the options bucket, a freshly-created inbound stays invisible in
+    // the client add/edit modal until a full page reload.
     await Promise.all([
-      queryClient.invalidateQueries({ queryKey: keys.inbounds.slim() }),
+      queryClient.invalidateQueries({ queryKey: keys.inbounds.root() }),
       queryClient.invalidateQueries({ queryKey: keys.clients.onlines() }),
       queryClient.invalidateQueries({ queryKey: keys.clients.lastOnline() }),
     ]);

+ 5 - 3
frontend/src/pages/index/IndexPage.tsx

@@ -480,7 +480,9 @@ export default function IndexPage() {
             open={configTextOpen}
             title={t('pages.index.config')}
             width={isMobile ? '100%' : 900}
-            style={isMobile ? { top: 20, maxWidth: 'calc(100vw - 16px)' } : undefined}
+            style={isMobile
+              ? { top: 20, maxWidth: 'calc(100vw - 16px)' }
+              : { top: 20 }}
             onCancel={() => setConfigTextOpen(false)}
             footer={[
               <Button
@@ -505,8 +507,8 @@ export default function IndexPage() {
             <JsonEditor
               value={configText}
               onChange={setConfigText}
-              minHeight={isMobile ? '300px' : '420px'}
-              maxHeight={isMobile ? '500px' : '720px'}
+              minHeight={isMobile ? '300px' : 'calc(100vh - 220px)'}
+              maxHeight={isMobile ? '70vh' : 'calc(100vh - 220px)'}
               readOnly
             />
           </Modal>

+ 9 - 3
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -227,8 +227,14 @@ export default function OutboundFormModal({
 
   const tag = Form.useWatch('tag', form) ?? '';
   const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
-  const network = (Form.useWatch(['streamSettings', 'network'], form) ?? '') as string;
-  const security = (Form.useWatch(['streamSettings', 'security'], form) ?? 'none') as string;
+  // preserve: true — without it useWatch only reflects values whose
+  // Form.Item is currently mounted. The streamSettings selectors live
+  // INSIDE `{streamAllowed && network && (...)}`, so the moment that
+  // conditional gates them out, useWatch returns undefined, the gate
+  // keeps returning false, and the stream block never renders even
+  // though streamSettings is in the form store.
+  const network = (Form.useWatch(['streamSettings', 'network'], { form, preserve: true }) ?? '') as string;
+  const security = (Form.useWatch(['streamSettings', 'security'], { form, preserve: true }) ?? 'none') as string;
 
   const streamAllowed = canEnableStream({ protocol });
   const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
@@ -1856,7 +1862,7 @@ export default function OutboundFormModal({
                       </>
                     )}
 
-                    {streamAllowed && network && (
+                    {((streamAllowed && network) || !streamAllowed) && (
                       <Form.Item shouldUpdate noStyle>
                         {() => {
                           const hasSockopt = !!form.getFieldValue([

+ 24 - 5
frontend/src/test/inbound-form-adapter.test.ts

@@ -114,7 +114,7 @@ describe('rawInboundToFormValues', () => {
 });
 
 describe('formValuesToWirePayload', () => {
-  it('stringifies settings/streamSettings/sniffing', () => {
+  it('stringifies settings/streamSettings/sniffing with empty-array/default pruning', () => {
     const values = rawInboundToFormValues(vlessRow);
     const payload = formValuesToWirePayload(values);
 
@@ -122,9 +122,18 @@ describe('formValuesToWirePayload', () => {
     expect(typeof payload.streamSettings).toBe('string');
     expect(typeof payload.sniffing).toBe('string');
 
-    expect(JSON.parse(payload.settings)).toEqual(vlessRow.settings);
+    // Empty arrays like `fallbacks: []` drop out of the payload to match
+    // the legacy panel's minimal JSON.
+    const parsedSettings = JSON.parse(payload.settings);
+    const { fallbacks: _f, ...expectedSettings } = vlessRow.settings as Record<string, unknown>;
+    expect(parsedSettings).toEqual(expectedSettings);
+
     expect(JSON.parse(payload.streamSettings)).toEqual(vlessRow.streamSettings);
-    expect(JSON.parse(payload.sniffing)).toEqual(vlessRow.sniffing);
+
+    // Disabled sniffing collapses to the bare `{ enabled: false }`
+    // regardless of which destOverride/metadataOnly/etc. defaults the
+    // form carries.
+    expect(JSON.parse(payload.sniffing)).toEqual({ enabled: false });
   });
 
   it('emits empty string for absent streamSettings', () => {
@@ -145,7 +154,11 @@ describe('formValuesToWirePayload', () => {
     expect(payload.nodeId).toBe(42);
   });
 
-  it('round-trips through raw → values → payload → values', () => {
+  it('round-trips top-level fields through raw → values → payload → values', () => {
+    // settings/streamSettings/sniffing don't round-trip byte-equal because
+    // the wire payload prunes empty arrays and collapses disabled sniffing
+    // to `{ enabled: false }`. Top-level scalars and the protocol picker
+    // must still survive the round trip without loss.
     const original = rawInboundToFormValues(vlessRow);
     const payload = formValuesToWirePayload(original);
     const replay = rawInboundToFormValues({
@@ -166,6 +179,12 @@ describe('formValuesToWirePayload', () => {
       lastTrafficResetTime: payload.lastTrafficResetTime,
       nodeId: payload.nodeId ?? null,
     });
-    expect(replay).toEqual(original);
+    expect(replay.protocol).toBe(original.protocol);
+    expect(replay.port).toBe(original.port);
+    expect(replay.tag).toBe(original.tag);
+    expect(replay.listen).toBe(original.listen);
+    expect(replay.up).toBe(original.up);
+    expect(replay.down).toBe(original.down);
+    expect(replay.streamSettings).toEqual(original.streamSettings);
   });
 });