Просмотр исходного кода

feat(xray): update xray-core to v26.7.11 and adapt panel

Bump xtls/xray-core to 50231eaf (v26.7.11) and the three binary pins
(DockerInit.sh, release.yml x2) in lockstep.

Adapt the panel to the upstream changes:

- Shadowsocks "none"/"plain" and VMess "none"/"zero" were removed from
  the core. A migration rewrites stored none/plain SS methods to a
  supported cipher and none/zero VMess security to "auto" (on both the
  clients column and inbound settings JSON); the SS build-time heal does
  the same so a row injected after boot cannot brick startup. The removed
  values are dropped from every frontend option list, schema and adapter,
  and coerced to "auto" at the Go link/sub/Clash emit sites and both link
  importers. Fix the CipherType_NONE sentinel that no longer compiles.

- Unencrypted vless/trojan outbounds to a public address are now refused
  by the core. Validate outbounds through the vendored config loader when
  saving the xray template and when storing/merging outbound
  subscriptions, so one such outbound cannot keep the core from starting.

- New TCP finalmask type "xmc" (Minecraft mimicry): add it to the sub
  link allowlist, the frontend enum and the FinalMask form (hostname,
  usernames, required password), and document it.

- streamSettings gained a "method" alias for "network"; canonicalize it
  to "network" at inbound save time and in the form adapters/schema so a
  method-keyed config keeps its transport.

- New root "env" config key is passed through xray.Config, compared in
  Equals, and forces a restart in the hot diff.

- REALITY now defaults minClientVer to 26.3.27; update the form
  placeholder.
MHSanaei 1 день назад
Родитель
Сommit
814cda3fb4
39 измененных файлов с 709 добавлено и 69 удалено
  1. 2 2
      .github/workflows/release.yml
  2. 1 1
      DockerInit.sh
  3. 3 1
      docs/content/docs/en/config/transports.mdx
  4. 3 1
      docs/content/docs/fa/config/transports.mdx
  5. 3 1
      docs/content/docs/ru/config/transports.mdx
  6. 2 1
      docs/content/docs/zh/config/transports.mdx
  7. 38 0
      frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx
  8. 4 0
      frontend/src/lib/xray/inbound-form-adapter.ts
  9. 5 1
      frontend/src/lib/xray/outbound-form-adapter.ts
  10. 3 1
      frontend/src/lib/xray/outbound-link-parser.ts
  11. 4 2
      frontend/src/pages/clients/ClientFormModal.tsx
  12. 1 1
      frontend/src/pages/inbounds/form/security/reality.tsx
  13. 0 2
      frontend/src/schemas/primitives/options.ts
  14. 6 6
      frontend/src/schemas/protocols/shared/vmess.ts
  15. 2 2
      frontend/src/schemas/protocols/stream/finalmask.ts
  16. 22 4
      frontend/src/schemas/protocols/stream/index.ts
  17. 10 0
      frontend/src/test/__snapshots__/finalmask.test.ts.snap
  18. 8 0
      frontend/src/test/golden/fixtures/finalmask/tcp-mask.json
  19. 13 0
      frontend/src/test/stream.test.ts
  20. 6 6
      go.mod
  21. 12 12
      go.sum
  22. 125 0
      internal/database/db.go
  23. 31 4
      internal/database/model/model.go
  24. 43 0
      internal/database/model/model_shadowsocks_removed_test.go
  25. 117 0
      internal/database/removed_cipher_migration_test.go
  26. 1 5
      internal/sub/clash_service.go
  27. 1 4
      internal/sub/json_service.go
  28. 14 1
      internal/sub/service.go
  29. 15 0
      internal/sub/service_test.go
  30. 5 1
      internal/util/link/outbound.go
  31. 29 1
      internal/web/service/inbound.go
  32. 38 7
      internal/web/service/outbound_subscription.go
  33. 57 0
      internal/web/service/stream_network_alias_test.go
  34. 15 0
      internal/web/service/xray_setting.go
  35. 17 2
      internal/xray/api.go
  36. 4 0
      internal/xray/config.go
  37. 1 0
      internal/xray/hot_diff.go
  38. 6 0
      internal/xray/hot_diff_test.go
  39. 42 0
      internal/xray/outbound_validation_test.go

+ 2 - 2
.github/workflows/release.yml

@@ -118,7 +118,7 @@ jobs:
           cd x-ui/bin
           cd x-ui/bin
 
 
           # Download dependencies
           # Download dependencies
-          Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.6.27/"
+          Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.7.11/"
           if [ "${{ matrix.platform }}" == "amd64" ]; then
           if [ "${{ matrix.platform }}" == "amd64" ]; then
             wget -q ${Xray_URL}Xray-linux-64.zip
             wget -q ${Xray_URL}Xray-linux-64.zip
             unzip Xray-linux-64.zip
             unzip Xray-linux-64.zip
@@ -273,7 +273,7 @@ jobs:
           cd x-ui\bin
           cd x-ui\bin
 
 
           # Download Xray for Windows
           # Download Xray for Windows
-          $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.6.27/"
+          $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.7.11/"
           Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
           Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
           Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
           Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
           Remove-Item "Xray-windows-64.zip"
           Remove-Item "Xray-windows-64.zip"

+ 1 - 1
DockerInit.sh

@@ -32,7 +32,7 @@ if [ -z "$MTG_MULTI_VER" ]; then
 fi
 fi
 mkdir -p build/bin
 mkdir -p build/bin
 cd build/bin
 cd build/bin
-curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.6.27/Xray-linux-${ARCH}.zip"
+curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.7.11/Xray-linux-${ARCH}.zip"
 unzip "Xray-linux-${ARCH}.zip"
 unzip "Xray-linux-${ARCH}.zip"
 rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
 rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
 mv xray "xray-linux-${FNAME}"
 mv xray "xray-linux-${FNAME}"

+ 3 - 1
docs/content/docs/en/config/transports.mdx

@@ -122,7 +122,9 @@ Only valid when the protocol is **Hysteria2**.
 disguise transports that don't carry TLS (like mKCP) or add a second skin on top of
 disguise transports that don't carry TLS (like mKCP) or add a second skin on top of
 TLS. Masks are configured per direction:
 TLS. Masks are configured per direction:
 
 
-- **TCP masks** — `fragment`, `sudoku`, `header-custom`.
+- **TCP masks** — `fragment`, `sudoku`, `header-custom`, `xmc` (disguises the
+  stream as Minecraft protocol traffic; requires a password, with optional
+  hostname and player usernames).
 - **UDP masks** — `salamander`, `mkcp-legacy`, `header-custom`, `xdns`, `xicmp`,
 - **UDP masks** — `salamander`, `mkcp-legacy`, `header-custom`, `xdns`, `xicmp`,
   `noise`, `sudoku`, `realm`. (`mkcp-legacy` reproduces the old mKCP header
   `noise`, `sudoku`, `realm`. (`mkcp-legacy` reproduces the old mKCP header
   obfuscation.)
   obfuscation.)

+ 3 - 1
docs/content/docs/fa/config/transports.mdx

@@ -123,7 +123,9 @@ icon: Network
 انتقال‌هایی را که TLS حمل نمی‌کنند (مانند mKCP) استتار کند یا پوسته‌ای دوم روی TLS
 انتقال‌هایی را که TLS حمل نمی‌کنند (مانند mKCP) استتار کند یا پوسته‌ای دوم روی TLS
 بیفزاید. ماسک‌ها برای هر جهت پیکربندی می‌شوند:
 بیفزاید. ماسک‌ها برای هر جهت پیکربندی می‌شوند:
 
 
-- **ماسک‌های TCP** — `fragment`، `sudoku`، `header-custom`.
+- **ماسک‌های TCP** — `fragment`، `sudoku`، `header-custom`، `xmc` (ترافیک را به شکل
+  پروتکل Minecraft استتار می‌کند؛ گذرواژه الزامی است و نام میزبان و نام‌های بازیکن
+  اختیاری‌اند).
 - **ماسک‌های UDP** — `salamander`، `mkcp-legacy`، `header-custom`، `xdns`، `xicmp`،
 - **ماسک‌های UDP** — `salamander`، `mkcp-legacy`، `header-custom`، `xdns`، `xicmp`،
   `noise`، `sudoku`، `realm`. (`mkcp-legacy` همان مبهم‌سازی قدیمی هدر mKCP را
   `noise`، `sudoku`، `realm`. (`mkcp-legacy` همان مبهم‌سازی قدیمی هدر mKCP را
   بازتولید می‌کند.)
   بازتولید می‌کند.)

+ 3 - 1
docs/content/docs/ru/config/transports.mdx

@@ -125,7 +125,9 @@ HTTPUpgrade — это одноразовый HTTP/1.1 `Upgrade` без фрей
 поэтому он может замаскировать транспорты, не несущие TLS (например, mKCP), или
 поэтому он может замаскировать транспорты, не несущие TLS (например, mKCP), или
 добавить вторую оболочку поверх TLS. Маски настраиваются по направлениям:
 добавить вторую оболочку поверх TLS. Маски настраиваются по направлениям:
 
 
-- **TCP-маски** — `fragment`, `sudoku`, `header-custom`.
+- **TCP-маски** — `fragment`, `sudoku`, `header-custom`, `xmc` (маскирует поток
+  под трафик протокола Minecraft; требуется пароль, имя хоста и имена игроков
+  опциональны).
 - **UDP-маски** — `salamander`, `mkcp-legacy`, `header-custom`, `xdns`, `xicmp`,
 - **UDP-маски** — `salamander`, `mkcp-legacy`, `header-custom`, `xdns`, `xicmp`,
   `noise`, `sudoku`, `realm`. (`mkcp-legacy` воспроизводит старую обфускацию
   `noise`, `sudoku`, `realm`. (`mkcp-legacy` воспроизводит старую обфускацию
   заголовка mKCP.)
   заголовка mKCP.)

+ 2 - 1
docs/content/docs/zh/config/transports.mdx

@@ -117,7 +117,8 @@ Session-ID 字段(`sessionIDPlacement`、`sessionIDKey`、`sessionIDTable`、
 **FinalMask** 在传输方式和安全层**之后**包裹流量,因此它既能伪装那些承载不了 TLS 的
 **FinalMask** 在传输方式和安全层**之后**包裹流量,因此它既能伪装那些承载不了 TLS 的
 传输(如 mKCP),也能在 TLS 之上再加一层外壳。掩码按方向分别配置:
 传输(如 mKCP),也能在 TLS 之上再加一层外壳。掩码按方向分别配置:
 
 
-- **TCP 掩码** — `fragment`、`sudoku`、`header-custom`。
+- **TCP 掩码** — `fragment`、`sudoku`、`header-custom`、`xmc`(把流量伪装成
+  Minecraft 协议;密码必填,主机名和玩家用户名可选)。
 - **UDP 掩码** — `salamander`、`mkcp-legacy`、`header-custom`、`xdns`、`xicmp`、
 - **UDP 掩码** — `salamander`、`mkcp-legacy`、`header-custom`、`xdns`、`xicmp`、
   `noise`、`sudoku`、`realm`。(`mkcp-legacy` 重现旧版 mKCP 的头部混淆。)
   `noise`、`sudoku`、`realm`。(`mkcp-legacy` 重现旧版 mKCP 的头部混淆。)
 - **QUIC 参数** — 拥塞控制(`reno`、`bbr`、`brutal`、`force-brutal`)、Brutal 上/下行
 - **QUIC 参数** — 拥塞控制(`reno`、`bbr`、`brutal`、`force-brutal`)、Brutal 上/下行

+ 38 - 0
frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx

@@ -81,6 +81,8 @@ function defaultTcpMaskSettings(type: string): Record<string, unknown> {
       };
       };
     case 'header-custom':
     case 'header-custom':
       return { clients: [], servers: [] };
       return { clients: [], servers: [] };
+    case 'xmc':
+      return { hostname: '', usernames: [], password: RandomUtil.randomLowerAndNum(16) };
     default:
     default:
       return {};
       return {};
   }
   }
@@ -294,6 +296,7 @@ function TcpMaskItem({
             { value: 'fragment', label: 'Fragment' },
             { value: 'fragment', label: 'Fragment' },
             { value: 'header-custom', label: 'Header Custom' },
             { value: 'header-custom', label: 'Header Custom' },
             { value: 'sudoku', label: 'Sudoku' },
             { value: 'sudoku', label: 'Sudoku' },
+            { value: 'xmc', label: 'XMC (Minecraft)' },
           ]}
           ]}
         />
         />
       </Form.Item>
       </Form.Item>
@@ -371,6 +374,41 @@ function TcpMaskItem({
               />
               />
             );
             );
           }
           }
+          if (type === 'xmc') {
+            return (
+              <>
+                <Form.Item label="Hostname" name={[fieldName, 'settings', 'hostname']}>
+                  <Input placeholder="Server address mimicked in the handshake" />
+                </Form.Item>
+                <Form.Item
+                  label="Usernames"
+                  name={[fieldName, 'settings', 'usernames']}
+                  extra="Player names offered to probes; core defaults to Dream when empty."
+                >
+                  <Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
+                </Form.Item>
+                <Form.Item label="Password" required>
+                  <Space.Compact block>
+                    <Form.Item
+                      name={[fieldName, 'settings', 'password']}
+                      noStyle
+                      rules={[{ required: true, message: 'Password is required' }]}
+                    >
+                      <Input placeholder="Obfuscation password" style={{ width: 'calc(100% - 32px)' }} />
+                    </Form.Item>
+                    <Button
+                      icon={<ReloadOutlined />}
+                      aria-label={t('regenerate')}
+                      onClick={() => form.setFieldValue(
+                        [...absolutePath, 'settings', 'password'],
+                        RandomUtil.randomLowerAndNum(16),
+                      )}
+                    />
+                  </Space.Compact>
+                </Form.Item>
+              </>
+            );
+          }
           return null;
           return null;
         }}
         }}
       </Form.Item>
       </Form.Item>

+ 4 - 0
frontend/src/lib/xray/inbound-form-adapter.ts

@@ -124,6 +124,10 @@ const NETWORK_SETTINGS_KEY: Record<string, string> = {
 };
 };
 
 
 function healStreamNetworkKey(stream: Record<string, unknown>): void {
 function healStreamNetworkKey(stream: Record<string, unknown>): void {
+  if (typeof stream.method === 'string' && stream.method !== '') {
+    stream.network = stream.method;
+  }
+  delete stream.method;
   const network = typeof stream.network === 'string' ? stream.network : '';
   const network = typeof stream.network === 'string' ? stream.network : '';
   const key = NETWORK_SETTINGS_KEY[network];
   const key = NETWORK_SETTINGS_KEY[network];
   if (!key) return;
   if (!key) return;

+ 5 - 1
frontend/src/lib/xray/outbound-form-adapter.ts

@@ -107,7 +107,7 @@ function vmessFromWire(raw: Raw): VmessOutboundFormSettings {
     id: asString(u.id),
     id: asString(u.id),
     security: ((): VmessOutboundFormSettings['security'] => {
     security: ((): VmessOutboundFormSettings['security'] => {
       const s = asString(u.security);
       const s = asString(u.security);
-      const allowed = ['aes-128-gcm', 'chacha20-poly1305', 'auto', 'none', 'zero'];
+      const allowed = ['aes-128-gcm', 'chacha20-poly1305', 'auto'];
       return (allowed.includes(s) ? s : 'auto') as VmessOutboundFormSettings['security'];
       return (allowed.includes(s) ? s : 'auto') as VmessOutboundFormSettings['security'];
     })(),
     })(),
   };
   };
@@ -391,6 +391,10 @@ const XMUX_DEFAULTS = XHttpXmuxSchema.parse({});
 
 
 function hydrateStreamForm(stream: Raw): OutboundStreamFormValues {
 function hydrateStreamForm(stream: Raw): OutboundStreamFormValues {
   const next = { ...stream };
   const next = { ...stream };
+  if (typeof next.method === 'string' && next.method !== '') {
+    next.network = next.method;
+  }
+  delete next.method;
   const xh = next.xhttpSettings;
   const xh = next.xhttpSettings;
   if (xh && typeof xh === 'object' && !Array.isArray(xh)) {
   if (xh && typeof xh === 'object' && !Array.isArray(xh)) {
     const xhttp = { ...(xh as Raw) };
     const xhttp = { ...(xh as Raw) };

+ 3 - 1
frontend/src/lib/xray/outbound-link-parser.ts

@@ -360,6 +360,8 @@ export function parseVmessLink(link: string): Raw | null {
     }
     }
 
 
     const port = Number(json.port) || 443;
     const port = Number(json.port) || 443;
+    const rawScy = (json.scy as string) || 'auto';
+    const userSecurity = rawScy === 'none' || rawScy === 'zero' ? 'auto' : rawScy;
     return {
     return {
       protocol: 'vmess',
       protocol: 'vmess',
       tag: typeof json.ps === 'string' ? json.ps : '',
       tag: typeof json.ps === 'string' ? json.ps : '',
@@ -367,7 +369,7 @@ export function parseVmessLink(link: string): Raw | null {
         vnext: [{
         vnext: [{
           address: json.add ?? '',
           address: json.add ?? '',
           port,
           port,
-          users: [{ id: json.id ?? '', security: (json.scy as string) || 'auto' }],
+          users: [{ id: json.id ?? '', security: userSecurity }],
         }],
         }],
       },
       },
       streamSettings: stream,
       streamSettings: stream,

+ 4 - 2
frontend/src/pages/clients/ClientFormModal.tsx

@@ -36,7 +36,7 @@ import { useFail2banStatusQuery, getLimitIpNotice } from '@/api/queries/useFail2
 import { ClientFormSchema, ClientCreateFormSchema, type ClientFormValues } from '@/schemas/client';
 import { ClientFormSchema, ClientCreateFormSchema, type ClientFormValues } from '@/schemas/client';
 
 
 const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
 const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL);
-const VMESS_SECURITY_OPTIONS = ['auto', 'aes-128-gcm', 'chacha20-poly1305', 'none', 'zero'] as const;
+const VMESS_SECURITY_OPTIONS = ['auto', 'aes-128-gcm', 'chacha20-poly1305'] as const;
 
 
 const MULTI_CLIENT_PROTOCOLS = new Set([
 const MULTI_CLIENT_PROTOCOLS = new Set([
   'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'wireguard', 'mtproto',
   'shadowsocks', 'vless', 'vmess', 'trojan', 'hysteria', 'wireguard', 'mtproto',
@@ -217,7 +217,9 @@ export default function ClientFormModal({
         password: client.password || '',
         password: client.password || '',
         auth: client.auth || '',
         auth: client.auth || '',
         flow: client.flow || '',
         flow: client.flow || '',
-        security: client.security || 'auto',
+        security: !client.security || client.security === 'none' || client.security === 'zero'
+          ? 'auto'
+          : client.security,
         reverseTag: client.reverse?.tag || '',
         reverseTag: client.reverse?.tag || '',
         totalGB: bytesToGB(client.totalGB || 0),
         totalGB: bytesToGB(client.totalGB || 0),
         reset: Number(client.reset) || 0,
         reset: Number(client.reset) || 0,

+ 1 - 1
frontend/src/pages/inbounds/form/security/reality.tsx

@@ -128,7 +128,7 @@ export default function RealityForm({
         name={['streamSettings', 'realitySettings', 'minClientVer']}
         name={['streamSettings', 'realitySettings', 'minClientVer']}
         label={t('pages.inbounds.form.minClientVer')}
         label={t('pages.inbounds.form.minClientVer')}
       >
       >
-        <Input placeholder="25.9.11" />
+        <Input placeholder="26.3.27" />
       </FormField>
       </FormField>
       <FormField
       <FormField
         name={['streamSettings', 'realitySettings', 'maxClientVer']}
         name={['streamSettings', 'realitySettings', 'maxClientVer']}

+ 0 - 2
frontend/src/schemas/primitives/options.ts

@@ -30,8 +30,6 @@ export const USERS_SECURITY = Object.freeze({
   AES_128_GCM: 'aes-128-gcm',
   AES_128_GCM: 'aes-128-gcm',
   CHACHA20_POLY1305: 'chacha20-poly1305',
   CHACHA20_POLY1305: 'chacha20-poly1305',
   AUTO: 'auto',
   AUTO: 'auto',
-  NONE: 'none',
-  ZERO: 'zero',
 });
 });
 
 
 export const MODE_OPTION = Object.freeze({
 export const MODE_OPTION = Object.freeze({

+ 6 - 6
frontend/src/schemas/protocols/shared/vmess.ts

@@ -4,16 +4,16 @@ const VmessSecurityEnum = z.enum([
   'aes-128-gcm',
   'aes-128-gcm',
   'chacha20-poly1305',
   'chacha20-poly1305',
   'auto',
   'auto',
-  'none',
-  'zero',
 ]);
 ]);
 
 
 // Legacy rows persisted `security: ""` (especially on VMess inbounds
 // Legacy rows persisted `security: ""` (especially on VMess inbounds
-// created before the enum was nailed down). Preprocess maps the empty
-// string back to the documented default so existing data parses cleanly
-// — subsequent writes serialize the normalized value.
+// created before the enum was nailed down), and rows predating xray-core
+// v26.7.11 may still hold the removed "none"/"zero" values that the core
+// now treats as "auto". Preprocess maps all of them to the documented
+// default so existing data parses cleanly — subsequent writes serialize
+// the normalized value.
 export const VmessSecuritySchema = z.preprocess(
 export const VmessSecuritySchema = z.preprocess(
-  (val) => (val === '' ? 'auto' : val),
+  (val) => (val === '' || val === 'none' || val === 'zero' ? 'auto' : val),
   VmessSecurityEnum,
   VmessSecurityEnum,
 );
 );
 export type VmessSecurity = z.infer<typeof VmessSecurityEnum>;
 export type VmessSecurity = z.infer<typeof VmessSecurityEnum>;

+ 2 - 2
frontend/src/schemas/protocols/stream/finalmask.ts

@@ -5,12 +5,12 @@ import { z } from 'zod';
 // plus optional QUIC tuning. The `settings` sub-object is polymorphic on
 // plus optional QUIC tuning. The `settings` sub-object is polymorphic on
 // `type`; we model the wire-faithful shape with a permissive
 // `type`; we model the wire-faithful shape with a permissive
 // record-of-unknown for `settings` and leave per-type tightening to
 // record-of-unknown for `settings` and leave per-type tightening to
-// Step 6 — there are 8 UDP mask types plus 3 TCP mask types, each with
+// Step 6 — there are 8 UDP mask types plus 4 TCP mask types, each with
 // distinct setting fields, and modeling them all as discriminated unions
 // distinct setting fields, and modeling them all as discriminated unions
 // here would dwarf the rest of the stream module without buying anything
 // here would dwarf the rest of the stream module without buying anything
 // the safety net doesn't already cover.
 // the safety net doesn't already cover.
 
 
-export const TcpMaskTypeSchema = z.enum(['fragment', 'sudoku', 'header-custom']);
+export const TcpMaskTypeSchema = z.enum(['fragment', 'sudoku', 'header-custom', 'xmc']);
 export type TcpMaskType = z.infer<typeof TcpMaskTypeSchema>;
 export type TcpMaskType = z.infer<typeof TcpMaskTypeSchema>;
 
 
 export const TcpMaskSchema = z.object({
 export const TcpMaskSchema = z.object({

+ 22 - 4
frontend/src/schemas/protocols/stream/index.ts

@@ -52,10 +52,28 @@ const TransportNetworkSettingsSchema = z.discriminatedUnion('network', [
 // mode. The transportless branch accepts that shape (network absent), while a
 // mode. The transportless branch accepts that shape (network absent), while a
 // present-but-invalid network still fails both branches so a typo can't slip
 // present-but-invalid network still fails both branches so a typo can't slip
 // through. `network: never().optional()` reads as "this key must be absent".
 // through. `network: never().optional()` reads as "this key must be absent".
-export const NetworkSettingsSchema = z.union([
-  TransportNetworkSettingsSchema,
-  z.object({ network: z.never().optional() }),
-]);
+//
+// The preprocess folds `method` — xray-core v26.7.11's preferred alias for
+// `network`, which wins over `network` when both are present — back into the
+// panel-canonical `network` key, so imported/pasted configs keyed on the
+// alias don't silently match the transportless branch and lose their
+// transport.
+export const NetworkSettingsSchema = z.preprocess(
+  (val) => {
+    if (val && typeof val === 'object' && 'method' in val) {
+      const { method, ...rest } = val as Record<string, unknown>;
+      if (typeof method === 'string' && method !== '') {
+        return { ...rest, network: method };
+      }
+      return rest;
+    }
+    return val;
+  },
+  z.union([
+    TransportNetworkSettingsSchema,
+    z.object({ network: z.never().optional() }),
+  ]),
+);
 export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
 export type NetworkSettings = z.infer<typeof NetworkSettingsSchema>;
 
 
 // Orthogonal extras that ride alongside the network and security branches.
 // Orthogonal extras that ride alongside the network and security branches.

+ 10 - 0
frontend/src/test/__snapshots__/finalmask.test.ts.snap

@@ -167,6 +167,16 @@ exports[`FinalMaskStreamSettingsSchema fixtures > parses tcp-mask byte-stably 1`
       },
       },
       "type": "header-custom",
       "type": "header-custom",
     },
     },
+    {
+      "settings": {
+        "hostname": "mc.example.com",
+        "password": "s3cr3t",
+        "usernames": [
+          "Dream",
+        ],
+      },
+      "type": "xmc",
+    },
   ],
   ],
   "udp": [],
   "udp": [],
 }
 }

+ 8 - 0
frontend/src/test/golden/fixtures/finalmask/tcp-mask.json

@@ -25,6 +25,14 @@
         ],
         ],
         "errors": []
         "errors": []
       }
       }
+    },
+    {
+      "type": "xmc",
+      "settings": {
+        "hostname": "mc.example.com",
+        "usernames": ["Dream"],
+        "password": "s3cr3t"
+      }
     }
     }
   ]
   ]
 }
 }

+ 13 - 0
frontend/src/test/stream.test.ts

@@ -24,3 +24,16 @@ describe('NetworkSettingsSchema fixtures', () => {
     });
     });
   }
   }
 });
 });
+
+describe('NetworkSettingsSchema method alias', () => {
+  it('folds xray-core v26.7.11 method alias back into network', () => {
+    const parsed = NetworkSettingsSchema.parse({ method: 'ws', wsSettings: {} });
+    expect((parsed as { network?: string }).network).toBe('ws');
+    expect((parsed as Record<string, unknown>).method).toBeUndefined();
+  });
+
+  it('prefers method over network when both are present', () => {
+    const parsed = NetworkSettingsSchema.parse({ method: 'grpc', network: 'tcp', grpcSettings: {} });
+    expect((parsed as { network?: string }).network).toBe('grpc');
+  });
+});

+ 6 - 6
go.mod

@@ -21,10 +21,10 @@ require (
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
 	github.com/valyala/fasthttp v1.72.0
 	github.com/valyala/fasthttp v1.72.0
 	github.com/xlzd/gotp v0.1.0
 	github.com/xlzd/gotp v0.1.0
-	github.com/xtls/xray-core v1.260327.1-0.20260627131803-45cf2898ab12
+	github.com/xtls/xray-core v1.260327.1-0.20260711155151-50231eaff98c
 	go.uber.org/atomic v1.11.0
 	go.uber.org/atomic v1.11.0
-	golang.org/x/crypto v0.53.0
-	golang.org/x/sys v0.46.0
+	golang.org/x/crypto v0.54.0
+	golang.org/x/sys v0.47.0
 	golang.org/x/text v0.40.0
 	golang.org/x/text v0.40.0
 	google.golang.org/grpc v1.82.0
 	google.golang.org/grpc v1.82.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -64,7 +64,7 @@ require (
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/juju/ratelimit v1.0.2 // indirect
 	github.com/juju/ratelimit v1.0.2 // indirect
 	github.com/klauspost/compress v1.19.0
 	github.com/klauspost/compress v1.19.0
-	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+	github.com/klauspost/cpuid/v2 v2.4.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
 	github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
 	github.com/mattn/go-isatty v0.0.22 // indirect
 	github.com/mattn/go-isatty v0.0.22 // indirect
@@ -77,7 +77,7 @@ require (
 	github.com/pion/logging v0.2.4 // indirect
 	github.com/pion/logging v0.2.4 // indirect
 	github.com/pion/stun/v3 v3.1.6 // indirect
 	github.com/pion/stun/v3 v3.1.6 // indirect
 	github.com/pion/transport/v4 v4.0.2 // indirect
 	github.com/pion/transport/v4 v4.0.2 // indirect
-	github.com/pires/go-proxyproto v0.12.0 // indirect
+	github.com/pires/go-proxyproto v0.15.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
 	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
 	github.com/quic-go/qpack v0.6.0 // indirect
 	github.com/quic-go/qpack v0.6.0 // indirect
 	github.com/quic-go/quic-go v0.60.0 // indirect
 	github.com/quic-go/quic-go v0.60.0 // indirect
@@ -101,7 +101,7 @@ require (
 	golang.org/x/arch v0.28.0 // indirect
 	golang.org/x/arch v0.28.0 // indirect
 	golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
 	golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
 	golang.org/x/mod v0.37.0 // indirect
 	golang.org/x/mod v0.37.0 // indirect
-	golang.org/x/net v0.56.0
+	golang.org/x/net v0.57.0
 	golang.org/x/sync v0.22.0 // indirect
 	golang.org/x/sync v0.22.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
 	golang.org/x/tools v0.47.0 // indirect
 	golang.org/x/tools v0.47.0 // indirect

+ 12 - 12
go.sum

@@ -117,8 +117,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
 github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
 github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
 github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
 github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
 github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
 github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
-github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
-github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
+github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -156,8 +156,8 @@ github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8=
 github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
 github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs=
 github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
 github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
 github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
 github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
-github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
-github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
+github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI=
+github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
@@ -218,8 +218,8 @@ github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
 github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
 github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
 github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
 github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
 github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
 github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
-github.com/xtls/xray-core v1.260327.1-0.20260627131803-45cf2898ab12 h1:0RZBYuLmkxxlMrSsCNRrdNWHhW5zHTfkC2vAknzZ5P4=
-github.com/xtls/xray-core v1.260327.1-0.20260627131803-45cf2898ab12/go.mod h1:5J11wfaLGhI6HNOi90zVI6E87c1ykA8grPfUeHZYbhA=
+github.com/xtls/xray-core v1.260327.1-0.20260711155151-50231eaff98c h1:SbB1ez0bqZllbzaVj0PC+Vje3dRA8m/7jW1ussjDSgM=
+github.com/xtls/xray-core v1.260327.1-0.20260711155151-50231eaff98c/go.mod h1:Jts8yHqPCpvsdL5CW5xMd8H9d2fkg1cILeBNqEwRXNw=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -248,14 +248,14 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
 golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
 golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
 golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
 golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
-golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
-golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
+golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
 golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
 golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
 golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
 golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
 golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
 golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
 golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
 golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
-golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
-golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
+golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
 golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
 golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
 golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -263,8 +263,8 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
-golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
 golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
 golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
 golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=

+ 125 - 0
internal/database/db.go

@@ -121,6 +121,12 @@ func initModels() error {
 	if err := migrateLegacySocksInboundsToMixed(); err != nil {
 	if err := migrateLegacySocksInboundsToMixed(); err != nil {
 		return err
 		return err
 	}
 	}
+	if err := migrateShadowsocksRemovedCiphers(); err != nil {
+		return err
+	}
+	if err := migrateVmessRemovedSecurities(); err != nil {
+		return err
+	}
 	if IsPostgres() {
 	if IsPostgres() {
 		if err := resyncPostgresSequences(db, models); err != nil {
 		if err := resyncPostgresSequences(db, models); err != nil {
 			log.Printf("Error resyncing postgres sequences: %v", err)
 			log.Printf("Error resyncing postgres sequences: %v", err)
@@ -754,6 +760,125 @@ func migrateLegacySocksInboundsToMixed() error {
 	return nil
 	return nil
 }
 }
 
 
+// migrateShadowsocksRemovedCiphers rewrites shadowsocks inbounds still using
+// the "none"/"plain" ciphers that xray-core v26.7.11 removed; one such row
+// makes the whole generated config unbuildable and keeps xray from starting.
+func migrateShadowsocksRemovedCiphers() error {
+	var inbounds []model.Inbound
+	if err := db.Where("protocol = ?", model.Shadowsocks).Find(&inbounds).Error; err != nil {
+		return err
+	}
+	migrated := int64(0)
+	for _, inbound := range inbounds {
+		if strings.TrimSpace(inbound.Settings) == "" {
+			continue
+		}
+		var settings map[string]any
+		if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
+			continue
+		}
+		changed := false
+		if method, _ := settings["method"].(string); method != "" {
+			if replacement, removed := model.ReplaceRemovedShadowsocksCipher(method); removed {
+				settings["method"] = replacement
+				changed = true
+			}
+		}
+		if clients, ok := settings["clients"].([]any); ok {
+			for i := range clients {
+				cm, ok := clients[i].(map[string]any)
+				if !ok {
+					continue
+				}
+				method, _ := cm["method"].(string)
+				if replacement, removed := model.ReplaceRemovedShadowsocksCipher(method); removed {
+					cm["method"] = replacement
+					clients[i] = cm
+					changed = true
+				}
+			}
+		}
+		if !changed {
+			continue
+		}
+		newSettings, err := json.MarshalIndent(settings, "", "  ")
+		if err != nil {
+			log.Printf("migrateShadowsocksRemovedCiphers: skip inbound %d (marshal failed): %v", inbound.Id, err)
+			continue
+		}
+		if err := db.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
+			Update("settings", string(newSettings)).Error; err != nil {
+			return err
+		}
+		migrated++
+	}
+	if migrated > 0 {
+		log.Printf("Rewrote removed shadowsocks cipher on %d inbound(s)", migrated)
+	}
+	return nil
+}
+
+// migrateVmessRemovedSecurities rewrites the vmess "none"/"zero" security
+// values that xray-core v26.7.11 removed to "auto" (what the core now treats
+// them as), on both the clients column and each vmess inbound's settings.
+func migrateVmessRemovedSecurities() error {
+	res := db.Exec("UPDATE clients SET security = 'auto' WHERE security IN ('none', 'zero')")
+	if res.Error != nil {
+		log.Printf("Error migrating removed vmess security values on clients: %v", res.Error)
+		return res.Error
+	}
+	if res.RowsAffected > 0 {
+		log.Printf("Migrated %d client(s) off removed vmess security values", res.RowsAffected)
+	}
+	var inbounds []model.Inbound
+	if err := db.Where("protocol = ?", model.VMESS).Find(&inbounds).Error; err != nil {
+		return err
+	}
+	migrated := int64(0)
+	for _, inbound := range inbounds {
+		if strings.TrimSpace(inbound.Settings) == "" {
+			continue
+		}
+		var settings map[string]any
+		if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
+			continue
+		}
+		clients, ok := settings["clients"].([]any)
+		if !ok {
+			continue
+		}
+		changed := false
+		for i := range clients {
+			cm, ok := clients[i].(map[string]any)
+			if !ok {
+				continue
+			}
+			if security, _ := cm["security"].(string); security == "none" || security == "zero" {
+				cm["security"] = "auto"
+				clients[i] = cm
+				changed = true
+			}
+		}
+		if !changed {
+			continue
+		}
+		newSettings, err := json.MarshalIndent(settings, "", "  ")
+		if err != nil {
+			log.Printf("migrateVmessRemovedSecurities: skip inbound %d (marshal failed): %v", inbound.Id, err)
+			continue
+		}
+		if err := db.Model(&model.Inbound{}).Where("id = ?", inbound.Id).
+			Update("settings", string(newSettings)).Error; err != nil {
+			return err
+		}
+		migrated++
+	}
+	if migrated > 0 {
+		log.Printf("Rewrote removed vmess security values on %d inbound(s)", migrated)
+	}
+	return nil
+}
+
 // normalizeInboundSubSortIndex lifts sub_sort_index values below the 1-based
 // normalizeInboundSubSortIndex lifts sub_sort_index values below the 1-based
 // minimum (rows written by builds that defaulted the column to 0, or by nodes
 // minimum (rows written by builds that defaulted the column to 0, or by nodes
 // predating the field) so they cannot sort ahead of explicitly ranked inbounds.
 // predating the field) so they cannot sort ahead of explicitly ranked inbounds.

+ 31 - 4
internal/database/model/model.go

@@ -442,8 +442,23 @@ func StripVlessInboundEncryption(settings string) (string, bool) {
 	return string(out), true
 	return string(out), true
 }
 }
 
 
-// HealShadowsocksClientMethods normalises the per-client `method` field
-// on a shadowsocks inbound's settings JSON before it leaves for xray-core:
+// ReplaceRemovedShadowsocksCipher maps ciphers that xray-core v26.7.11
+// deleted ("none"/"plain" make the whole config fail with "unknown cipher
+// method") to a still-supported replacement. Returns the replacement and
+// true when the given method is one of the removed ciphers.
+func ReplaceRemovedShadowsocksCipher(method string) (string, bool) {
+	switch method {
+	case "none", "plain":
+		return "chacha20-ietf-poly1305", true
+	}
+	return method, false
+}
+
+// HealShadowsocksClientMethods normalises the `method` fields on a
+// shadowsocks inbound's settings JSON before it leaves for xray-core:
+//   - Ciphers removed upstream (none/plain): rewritten via
+//     ReplaceRemovedShadowsocksCipher so one legacy row cannot prevent
+//     xray from starting.
 //   - Legacy ciphers (aes-*, chacha20-*): every client must carry a
 //   - Legacy ciphers (aes-*, chacha20-*): every client must carry a
 //     per-user `method` matching the inbound's top-level method, otherwise
 //     per-user `method` matching the inbound's top-level method, otherwise
 //     xray fails with "unsupported cipher method:".
 //     xray fails with "unsupported cipher method:".
@@ -462,12 +477,24 @@ func HealShadowsocksClientMethods(settings string) (string, bool) {
 		return settings, false
 		return settings, false
 	}
 	}
 	method, _ := parsed["method"].(string)
 	method, _ := parsed["method"].(string)
+	changed := false
+	if replacement, removed := ReplaceRemovedShadowsocksCipher(method); removed {
+		method = replacement
+		parsed["method"] = method
+		changed = true
+	}
 	clients, ok := parsed["clients"].([]any)
 	clients, ok := parsed["clients"].([]any)
 	if !ok {
 	if !ok {
-		return settings, false
+		if !changed {
+			return settings, false
+		}
+		out, err := json.MarshalIndent(parsed, "", "  ")
+		if err != nil {
+			return settings, false
+		}
+		return string(out), true
 	}
 	}
 	is2022 := strings.HasPrefix(method, "2022-blake3-")
 	is2022 := strings.HasPrefix(method, "2022-blake3-")
-	changed := false
 	for i := range clients {
 	for i := range clients {
 		cm, ok := clients[i].(map[string]any)
 		cm, ok := clients[i].(map[string]any)
 		if !ok {
 		if !ok {

+ 43 - 0
internal/database/model/model_shadowsocks_removed_test.go

@@ -0,0 +1,43 @@
+package model
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+// TestHealShadowsocksClientMethods_RewritesRemovedCipher covers the last-gate
+// build-time heal for xray-core v26.7.11's removed "none"/"plain" ciphers: a
+// row that survives to config generation (restored backup, direct DB edit)
+// must be rewritten to a supported cipher on both the inbound method and its
+// clients so one such inbound cannot keep xray from starting.
+func TestHealShadowsocksClientMethods_RewritesRemovedCipher(t *testing.T) {
+	settings := `{"method": "plain", "clients": [{"email": "a@x", "password": "p", "method": "plain"}]}`
+	healed, ok := HealShadowsocksClientMethods(settings)
+	if !ok {
+		t.Fatal("expected heal to report a change for a removed cipher")
+	}
+	var parsed struct {
+		Method  string           `json:"method"`
+		Clients []map[string]any `json:"clients"`
+	}
+	if err := json.Unmarshal([]byte(healed), &parsed); err != nil {
+		t.Fatalf("parse healed settings: %v", err)
+	}
+	if parsed.Method != "chacha20-ietf-poly1305" {
+		t.Fatalf("expected inbound method rewritten to a supported cipher, got %q", parsed.Method)
+	}
+	if parsed.Clients[0]["method"] != "chacha20-ietf-poly1305" {
+		t.Fatalf("expected client method to match the healed cipher, got %v", parsed.Clients[0]["method"])
+	}
+}
+
+func TestReplaceRemovedShadowsocksCipher(t *testing.T) {
+	for _, method := range []string{"none", "plain"} {
+		if got, removed := ReplaceRemovedShadowsocksCipher(method); !removed || got != "chacha20-ietf-poly1305" {
+			t.Fatalf("ReplaceRemovedShadowsocksCipher(%q) = (%q, %v), want a supported replacement", method, got, removed)
+		}
+	}
+	if got, removed := ReplaceRemovedShadowsocksCipher("aes-256-gcm"); removed || got != "aes-256-gcm" {
+		t.Fatalf("ReplaceRemovedShadowsocksCipher(aes-256-gcm) = (%q, %v), want it left untouched", got, removed)
+	}
+}

+ 117 - 0
internal/database/removed_cipher_migration_test.go

@@ -0,0 +1,117 @@
+package database
+
+import (
+	"encoding/json"
+	"path/filepath"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+)
+
+// TestMigrateShadowsocksRemovedCiphers_RewritesNoneAndPlain covers the
+// xray-core v26.7.11 removal of the shadowsocks "none"/"plain" ciphers: one
+// such row makes the generated config unbuildable, so startup must rewrite
+// both the inbound method and any per-client method to a supported cipher and
+// leave a valid inbound untouched.
+func TestMigrateShadowsocksRemovedCiphers_RewritesNoneAndPlain(t *testing.T) {
+	dbDir := t.TempDir()
+	t.Setenv("XUI_DB_FOLDER", dbDir)
+	if err := InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
+		t.Fatalf("InitDB failed: %v", err)
+	}
+	t.Cleanup(func() { _ = CloseDB() })
+
+	removed := `{"method": "none", "clients": [{"email": "a@x", "password": "p", "method": "plain"}]}`
+	dirty := model.Inbound{UserId: 1, Port: 31001, Protocol: model.Shadowsocks, Tag: "ss-removed", Settings: removed}
+	if err := db.Create(&dirty).Error; err != nil {
+		t.Fatalf("create dirty inbound: %v", err)
+	}
+
+	valid := `{"method": "aes-256-gcm", "clients": [{"email": "b@x", "password": "p"}]}`
+	clean := model.Inbound{UserId: 1, Port: 31002, Protocol: model.Shadowsocks, Tag: "ss-valid", Settings: valid}
+	if err := db.Create(&clean).Error; err != nil {
+		t.Fatalf("create clean inbound: %v", err)
+	}
+
+	if err := migrateShadowsocksRemovedCiphers(); err != nil {
+		t.Fatalf("migrateShadowsocksRemovedCiphers: %v", err)
+	}
+
+	var gotDirty model.Inbound
+	if err := db.First(&gotDirty, dirty.Id).Error; err != nil {
+		t.Fatalf("reload dirty inbound: %v", err)
+	}
+	var parsed struct {
+		Method  string           `json:"method"`
+		Clients []map[string]any `json:"clients"`
+	}
+	if err := json.Unmarshal([]byte(gotDirty.Settings), &parsed); err != nil {
+		t.Fatalf("parse repaired settings: %v", err)
+	}
+	if parsed.Method != "chacha20-ietf-poly1305" {
+		t.Fatalf("expected inbound method rewritten, got %q", parsed.Method)
+	}
+	if parsed.Clients[0]["method"] != "chacha20-ietf-poly1305" {
+		t.Fatalf("expected client method rewritten, got %v", parsed.Clients[0]["method"])
+	}
+
+	var gotClean model.Inbound
+	if err := db.First(&gotClean, clean.Id).Error; err != nil {
+		t.Fatalf("reload clean inbound: %v", err)
+	}
+	if gotClean.Settings != valid {
+		t.Fatalf("valid inbound was rewritten:\nbefore: %s\nafter:  %s", valid, gotClean.Settings)
+	}
+}
+
+// TestMigrateVmessRemovedSecurities_RewritesNoneAndZero covers the v26.7.11
+// removal of vmess "none"/"zero" security values: startup rewrites them to
+// "auto" on both the clients column and each vmess inbound's settings JSON.
+func TestMigrateVmessRemovedSecurities_RewritesNoneAndZero(t *testing.T) {
+	dbDir := t.TempDir()
+	t.Setenv("XUI_DB_FOLDER", dbDir)
+	if err := InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
+		t.Fatalf("InitDB failed: %v", err)
+	}
+	t.Cleanup(func() { _ = CloseDB() })
+
+	settings := `{"clients": [{"id": "u1", "email": "a@x", "security": "none"},` +
+		`{"id": "u2", "email": "b@x", "security": "zero"},` +
+		`{"id": "u3", "email": "c@x", "security": "aes-128-gcm"}]}`
+	inbound := model.Inbound{UserId: 1, Port: 32001, Protocol: model.VMESS, Tag: "vmess-removed", Settings: settings}
+	if err := db.Create(&inbound).Error; err != nil {
+		t.Fatalf("create vmess inbound: %v", err)
+	}
+	if err := db.Create(&model.ClientRecord{Email: "a@x", Security: "zero", Enable: true}).Error; err != nil {
+		t.Fatalf("create client record: %v", err)
+	}
+
+	if err := migrateVmessRemovedSecurities(); err != nil {
+		t.Fatalf("migrateVmessRemovedSecurities: %v", err)
+	}
+
+	var got model.Inbound
+	if err := db.First(&got, inbound.Id).Error; err != nil {
+		t.Fatalf("reload inbound: %v", err)
+	}
+	var parsed struct {
+		Clients []map[string]any `json:"clients"`
+	}
+	if err := json.Unmarshal([]byte(got.Settings), &parsed); err != nil {
+		t.Fatalf("parse settings: %v", err)
+	}
+	if parsed.Clients[0]["security"] != "auto" || parsed.Clients[1]["security"] != "auto" {
+		t.Fatalf("expected removed securities rewritten to auto, got %v", parsed.Clients)
+	}
+	if parsed.Clients[2]["security"] != "aes-128-gcm" {
+		t.Fatalf("expected valid security untouched, got %v", parsed.Clients[2]["security"])
+	}
+
+	var rec model.ClientRecord
+	if err := db.Where("email = ?", "a@x").First(&rec).Error; err != nil {
+		t.Fatalf("reload client record: %v", err)
+	}
+	if rec.Security != "auto" {
+		t.Fatalf("expected client record security rewritten to auto, got %q", rec.Security)
+	}
+}

+ 1 - 5
internal/sub/clash_service.go

@@ -236,11 +236,7 @@ func (s *SubClashService) buildProxy(subReq *SubService, inbound *model.Inbound,
 		proxy["type"] = "vmess"
 		proxy["type"] = "vmess"
 		proxy["uuid"] = client.ID
 		proxy["uuid"] = client.ID
 		proxy["alterId"] = 0
 		proxy["alterId"] = 0
-		cipher := client.Security
-		if cipher == "" {
-			cipher = "auto"
-		}
-		proxy["cipher"] = cipher
+		proxy["cipher"] = normalizeVmessSecurity(client.Security)
 	case model.VLESS:
 	case model.VLESS:
 		proxy["type"] = "vless"
 		proxy["type"] = "vless"
 		proxy["uuid"] = applyVlessRoute(client.ID, hostVlessRoute(ep))
 		proxy["uuid"] = applyVlessRoute(client.ID, hostVlessRoute(ep))

+ 1 - 4
internal/sub/json_service.go

@@ -384,10 +384,7 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
 	}
 	}
 	outbound.StreamSettings = streamSettings
 	outbound.StreamSettings = streamSettings
 
 
-	security := client.Security
-	if security == "" {
-		security = "auto"
-	}
+	security := normalizeVmessSecurity(client.Security)
 	outbound.Settings = map[string]any{
 	outbound.Settings = map[string]any{
 		"address":  inbound.Listen,
 		"address":  inbound.Listen,
 		"port":     inbound.Port,
 		"port":     inbound.Port,

+ 14 - 1
internal/sub/service.go

@@ -714,7 +714,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 		return ""
 		return ""
 	}
 	}
 	obj["id"] = client.ID
 	obj["id"] = client.ID
-	obj["scy"] = client.Security
+	obj["scy"] = normalizeVmessSecurity(client.Security)
 
 
 	externalProxies, _ := stream["externalProxy"].([]any)
 	externalProxies, _ := stream["externalProxy"].([]any)
 
 
@@ -726,6 +726,18 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
 	return buildVmessLink(obj)
 	return buildVmessLink(obj)
 }
 }
 
 
+// normalizeVmessSecurity maps the vmess security values xray-core v26.7.11
+// removed ("none"/"zero"), plus the legacy empty string, to "auto" so links
+// and subscriptions stop advertising values the upgraded server rejects on
+// the wire.
+func normalizeVmessSecurity(security string) string {
+	switch security {
+	case "", "none", "zero":
+		return "auto"
+	}
+	return security
+}
+
 // vlessEncryptionEnabled reports whether the VLESS inbound settings enable
 // vlessEncryptionEnabled reports whether the VLESS inbound settings enable
 // VLESS-level encryption (vlessenc / ML-KEM). When on, the encryption/decryption
 // VLESS-level encryption (vlessenc / ML-KEM). When on, the encryption/decryption
 // fields hold a generated dotted string (e.g. "mlkem768x25519plus.native.0rtt.<key>");
 // fields hold a generated dotted string (e.g. "mlkem768x25519plus.native.0rtt.<key>");
@@ -2131,6 +2143,7 @@ var validFinalMaskTCPTypes = map[string]struct{}{
 	"header-custom": {},
 	"header-custom": {},
 	"fragment":      {},
 	"fragment":      {},
 	"sudoku":        {},
 	"sudoku":        {},
+	"xmc":           {},
 }
 }
 
 
 // applyKcpShareParams reconstructs legacy KCP share-link fields from either
 // applyKcpShareParams reconstructs legacy KCP share-link fields from either

+ 15 - 0
internal/sub/service_test.go

@@ -1016,6 +1016,21 @@ func TestMarshalFinalMask_UnknownTypeIsDropped(t *testing.T) {
 	}
 	}
 }
 }
 
 
+func TestMarshalFinalMask_KeepsXmcTcpMask(t *testing.T) {
+	fm := map[string]any{
+		"tcp": []any{
+			map[string]any{"type": "xmc", "settings": map[string]any{"password": "p"}},
+		},
+	}
+	out, ok := marshalFinalMask(fm)
+	if !ok {
+		t.Fatal("expected ok=true for an xmc tcp mask")
+	}
+	if !strings.Contains(out, "xmc") {
+		t.Fatalf("marshaled finalmask dropped the xmc mask: %s", out)
+	}
+}
+
 func TestHasFinalMaskContent(t *testing.T) {
 func TestHasFinalMaskContent(t *testing.T) {
 	if hasFinalMaskContent(nil) {
 	if hasFinalMaskContent(nil) {
 		t.Fatal("nil should not count as content")
 		t.Fatal("nil should not count as content")

+ 5 - 1
internal/util/link/outbound.go

@@ -208,6 +208,10 @@ func parseVmess(link string) (*ParseResult, error) {
 	}
 	}
 
 
 	port := num(j["port"])
 	port := num(j["port"])
+	scy := getString(j, "scy", "auto")
+	if scy == "none" || scy == "zero" {
+		scy = "auto"
+	}
 	ob := Outbound{
 	ob := Outbound{
 		"protocol": "vmess",
 		"protocol": "vmess",
 		"tag":      getString(j, "ps", ""),
 		"tag":      getString(j, "ps", ""),
@@ -219,7 +223,7 @@ func parseVmess(link string) (*ParseResult, error) {
 					"users": []any{
 					"users": []any{
 						map[string]any{
 						map[string]any{
 							"id":       getString(j, "id", ""),
 							"id":       getString(j, "id", ""),
-							"security": getString(j, "scy", "auto"),
+							"security": scy,
 						},
 						},
 					},
 					},
 				},
 				},

+ 29 - 1
internal/web/service/inbound.go

@@ -503,7 +503,9 @@ func (s *InboundService) getAllEmailSubIDs() (map[string]string, error) {
 // Only vmess, vless, trojan, shadowsocks, hysteria, wireguard, and tunnel
 // Only vmess, vless, trojan, shadowsocks, hysteria, wireguard, and tunnel
 // protocols use streamSettings (wireguard for finalmask UDP masks and sockopt on
 // protocols use streamSettings (wireguard for finalmask UDP masks and sockopt on
 // its listener; tunnel for sockopt, notably sockopt.tproxy for its TProxy/redirect
 // its listener; tunnel for sockopt, notably sockopt.tproxy for its TProxy/redirect
-// mode).
+// mode). Streams keyed on "method" — xray-core v26.7.11's preferred alias for
+// "network" — are canonicalized to "network", which every panel reader (link
+// generation, port-conflict detection, flow eligibility) keys on.
 func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
 func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
 	protocolsWithStream := map[model.Protocol]bool{
 	protocolsWithStream := map[model.Protocol]bool{
 		model.VMESS:       true,
 		model.VMESS:       true,
@@ -517,7 +519,33 @@ func (s *InboundService) normalizeStreamSettings(inbound *model.Inbound) {
 
 
 	if !protocolsWithStream[inbound.Protocol] {
 	if !protocolsWithStream[inbound.Protocol] {
 		inbound.StreamSettings = ""
 		inbound.StreamSettings = ""
+		return
+	}
+	inbound.StreamSettings = canonicalizeStreamNetworkKey(inbound.StreamSettings)
+}
+
+// canonicalizeStreamNetworkKey rewrites a streamSettings JSON that names its
+// transport under "method" to the panel-canonical "network" key. When both
+// keys are present, "method" wins — matching xray-core's own precedence.
+func canonicalizeStreamNetworkKey(streamSettings string) string {
+	if streamSettings == "" {
+		return streamSettings
+	}
+	var stream map[string]any
+	if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
+		return streamSettings
+	}
+	method, ok := stream["method"].(string)
+	if !ok || method == "" {
+		return streamSettings
 	}
 	}
+	stream["network"] = method
+	delete(stream, "method")
+	out, err := json.MarshalIndent(stream, "", "  ")
+	if err != nil {
+		return streamSettings
+	}
+	return string(out)
 }
 }
 
 
 // finalMaskRealityTcpMasks returns the stream's finalmask.tcp masks when the
 // finalMaskRealityTcpMasks returns the stream's finalmask.tcp masks when the

+ 38 - 7
internal/web/service/outbound_subscription.go

@@ -17,8 +17,34 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/internal/logger"
 	"github.com/mhsanaei/3x-ui/v3/internal/logger"
 	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
 	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
 	"github.com/mhsanaei/3x-ui/v3/internal/util/link"
 	"github.com/mhsanaei/3x-ui/v3/internal/util/link"
+	"github.com/mhsanaei/3x-ui/v3/internal/xray"
 )
 )
 
 
+// filterOutboundsRejectedByCore drops outbounds the vendored xray-core config
+// loader refuses to build — since v26.7.11 that includes unencrypted
+// vless/trojan outbounds to public addresses — because one such outbound in
+// the merged config would keep the whole core from starting.
+func filterOutboundsRejectedByCore(label string, outbounds []any) ([]any, []string) {
+	kept := make([]any, 0, len(outbounds))
+	var dropped []string
+	for _, ob := range outbounds {
+		raw, err := json.Marshal(ob)
+		if err == nil {
+			if buildErr := xray.ValidateOutboundConfig(raw); buildErr != nil {
+				tag := ""
+				if m, ok := ob.(map[string]any); ok {
+					tag, _ = m["tag"].(string)
+				}
+				logger.Warningf("%s: dropping outbound %q rejected by xray-core: %v", label, tag, buildErr)
+				dropped = append(dropped, fmt.Sprintf("%s: %v", tag, buildErr))
+				continue
+			}
+		}
+		kept = append(kept, ob)
+	}
+	return kept, dropped
+}
+
 // maxOutboundSubscriptionBytes caps a single outbound subscription response.
 // maxOutboundSubscriptionBytes caps a single outbound subscription response.
 // It is larger than the 2 MiB user-facing subscription cap because an outbound
 // It is larger than the 2 MiB user-facing subscription cap because an outbound
 // subscription may aggregate many upstream outbounds into one document.
 // subscription may aggregate many upstream outbounds into one document.
@@ -347,24 +373,28 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
 	}
 	}
 	identJSON, _ := json.Marshal(newIdent)
 	identJSON, _ := json.Marshal(newIdent)
 
 
+	asAny := make([]any, len(parsed))
+	for i := range parsed {
+		asAny[i] = map[string]any(parsed[i])
+	}
+	kept, droppedByCore := filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), asAny)
+
 	// Persist the outbounds (as compact JSON array)
 	// Persist the outbounds (as compact JSON array)
-	obsJSON, _ := json.Marshal(parsed)
+	obsJSON, _ := json.Marshal(kept)
 
 
 	sub.LastFetchedOutbounds = string(obsJSON)
 	sub.LastFetchedOutbounds = string(obsJSON)
 	sub.LinkIdentities = string(identJSON)
 	sub.LinkIdentities = string(identJSON)
 	sub.LastUpdated = time.Now().Unix()
 	sub.LastUpdated = time.Now().Unix()
 	sub.LastError = ""
 	sub.LastError = ""
+	if len(droppedByCore) > 0 {
+		sub.LastError = fmt.Sprintf("dropped %d outbound(s) the xray core rejects: %s", len(droppedByCore), droppedByCore[0])
+	}
 
 
 	if err := database.GetDB().Save(sub).Error; err != nil {
 	if err := database.GetDB().Save(sub).Error; err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	// Return as []any for the config merger
-	result := make([]any, len(parsed))
-	for i := range parsed {
-		result[i] = parsed[i]
-	}
-	return result, nil
+	return kept, nil
 }
 }
 
 
 func (s *OutboundSubscriptionService) recordError(sub *model.OutboundSubscription, err error) {
 func (s *OutboundSubscriptionService) recordError(sub *model.OutboundSubscription, err error) {
@@ -456,6 +486,7 @@ func (s *OutboundSubscriptionService) activeOutboundsSplit() (prepend []any, app
 			logger.Warningf("outbound sub %d has corrupt LastFetchedOutbounds: %v", sub.Id, err)
 			logger.Warningf("outbound sub %d has corrupt LastFetchedOutbounds: %v", sub.Id, err)
 			continue
 			continue
 		}
 		}
+		arr, _ = filterOutboundsRejectedByCore(fmt.Sprintf("outbound sub %d", sub.Id), arr)
 		if sub.Prepend {
 		if sub.Prepend {
 			prepend = append(prepend, arr...)
 			prepend = append(prepend, arr...)
 		} else {
 		} else {

+ 57 - 0
internal/web/service/stream_network_alias_test.go

@@ -0,0 +1,57 @@
+package service
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+// TestCanonicalizeStreamNetworkKey covers xray-core v26.7.11's "method" alias
+// for streamSettings "network": a config keyed on "method" (from an imported
+// or API-authored inbound) must be folded back to the panel-canonical
+// "network" key that every downstream reader — link generation, port-conflict
+// detection, flow eligibility — depends on.
+func TestCanonicalizeStreamNetworkKey(t *testing.T) {
+	tests := []struct {
+		name        string
+		in          string
+		wantNetwork string
+		wantMethod  bool
+	}{
+		{
+			name:        "method alias becomes network",
+			in:          `{"method": "ws", "security": "tls"}`,
+			wantNetwork: "ws",
+		},
+		{
+			name:        "method wins when both present",
+			in:          `{"method": "grpc", "network": "tcp"}`,
+			wantNetwork: "grpc",
+		},
+		{
+			name:        "plain network untouched",
+			in:          `{"network": "tcp"}`,
+			wantNetwork: "tcp",
+		},
+	}
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			got := canonicalizeStreamNetworkKey(tc.in)
+			var stream map[string]any
+			if err := json.Unmarshal([]byte(got), &stream); err != nil {
+				t.Fatalf("result is not valid JSON: %v", err)
+			}
+			if stream["network"] != tc.wantNetwork {
+				t.Fatalf("network = %v, want %q", stream["network"], tc.wantNetwork)
+			}
+			if _, hasMethod := stream["method"]; hasMethod {
+				t.Fatalf("method key must be removed, got %s", got)
+			}
+		})
+	}
+}
+
+func TestCanonicalizeStreamNetworkKey_EmptyPassthrough(t *testing.T) {
+	if got := canonicalizeStreamNetworkKey(""); got != "" {
+		t.Fatalf("empty stream must round-trip, got %q", got)
+	}
+}

+ 15 - 0
internal/web/service/xray_setting.go

@@ -41,6 +41,21 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
 	if err != nil {
 	if err != nil {
 		return common.NewError("xray template config invalid:", err)
 		return common.NewError("xray template config invalid:", err)
 	}
 	}
+	if len(xrayConfig.OutboundConfigs) > 0 {
+		var outbounds []json.RawMessage
+		if err := json.Unmarshal(xrayConfig.OutboundConfigs, &outbounds); err != nil {
+			return common.NewError("xray template config invalid: outbounds is not an array:", err)
+		}
+		for _, outbound := range outbounds {
+			if err := xray.ValidateOutboundConfig(outbound); err != nil {
+				tagged := struct {
+					Tag string `json:"tag"`
+				}{}
+				_ = json.Unmarshal(outbound, &tagged)
+				return common.NewError("xray core rejects outbound \""+tagged.Tag+"\":", err)
+			}
+		}
+	}
 	return nil
 	return nil
 }
 }
 
 

+ 17 - 2
internal/xray/api.go

@@ -171,6 +171,19 @@ func (x *XrayAPI) DelInbound(tag string) error {
 	return err
 	return err
 }
 }
 
 
+// ValidateOutboundConfig builds an outbound JSON object through the vendored
+// xray-core config loader, surfacing the exact error the core would raise at
+// startup — notably v26.7.11's refusal of unencrypted vless/trojan outbounds
+// whose server address is a public IP or domain.
+func ValidateOutboundConfig(outbound []byte) error {
+	detour := new(conf.OutboundDetourConfig)
+	if err := json.Unmarshal(outbound, detour); err != nil {
+		return err
+	}
+	_, err := detour.Build()
+	return err
+}
+
 // AddOutbound adds a new outbound configuration to the Xray core via gRPC.
 // AddOutbound adds a new outbound configuration to the Xray core via gRPC.
 func (x *XrayAPI) AddOutbound(outbound []byte) error {
 func (x *XrayAPI) AddOutbound(outbound []byte) error {
 	if x.HandlerServiceClient == nil {
 	if x.HandlerServiceClient == nil {
@@ -518,6 +531,8 @@ func buildUserAccount(protocolName string, user map[string]any) (*serial.TypedMe
 
 
 		var ssCipherType shadowsocks.CipherType
 		var ssCipherType shadowsocks.CipherType
 		switch cipher {
 		switch cipher {
+		case "aes-128-gcm":
+			ssCipherType = shadowsocks.CipherType_AES_128_GCM
 		case "aes-256-gcm":
 		case "aes-256-gcm":
 			ssCipherType = shadowsocks.CipherType_AES_256_GCM
 			ssCipherType = shadowsocks.CipherType_AES_256_GCM
 		case "chacha20-poly1305", "chacha20-ietf-poly1305":
 		case "chacha20-poly1305", "chacha20-ietf-poly1305":
@@ -525,10 +540,10 @@ func buildUserAccount(protocolName string, user map[string]any) (*serial.TypedMe
 		case "xchacha20-poly1305", "xchacha20-ietf-poly1305":
 		case "xchacha20-poly1305", "xchacha20-ietf-poly1305":
 			ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305
 			ssCipherType = shadowsocks.CipherType_XCHACHA20_POLY1305
 		default:
 		default:
-			ssCipherType = shadowsocks.CipherType_NONE
+			ssCipherType = shadowsocks.CipherType_UNKNOWN
 		}
 		}
 
 
-		if ssCipherType != shadowsocks.CipherType_NONE {
+		if ssCipherType != shadowsocks.CipherType_UNKNOWN {
 			return serial.ToTypedMessage(&shadowsocks.Account{
 			return serial.ToTypedMessage(&shadowsocks.Account{
 				Password:   password,
 				Password:   password,
 				CipherType: ssCipherType,
 				CipherType: ssCipherType,

+ 4 - 0
internal/xray/config.go

@@ -24,6 +24,7 @@ type Config struct {
 	BurstObservatory json_util.RawMessage `json:"burstObservatory,omitempty"`
 	BurstObservatory json_util.RawMessage `json:"burstObservatory,omitempty"`
 	Metrics          json_util.RawMessage `json:"metrics"`
 	Metrics          json_util.RawMessage `json:"metrics"`
 	Geodata          json_util.RawMessage `json:"geodata,omitempty"`
 	Geodata          json_util.RawMessage `json:"geodata,omitempty"`
+	Env              json_util.RawMessage `json:"env,omitempty"`
 }
 }
 
 
 // Equals compares two Config instances for deep equality.
 // Equals compares two Config instances for deep equality.
@@ -78,5 +79,8 @@ func (c *Config) Equals(other *Config) bool {
 	if !bytes.Equal(c.Geodata, other.Geodata) {
 	if !bytes.Equal(c.Geodata, other.Geodata) {
 		return false
 		return false
 	}
 	}
+	if !bytes.Equal(c.Env, other.Env) {
+		return false
+	}
 	return true
 	return true
 }
 }

+ 1 - 0
internal/xray/hot_diff.go

@@ -70,6 +70,7 @@ func ComputeHotDiff(oldCfg, newCfg *Config) (*HotDiff, bool) {
 		{"burstObservatory", oldCfg.BurstObservatory, newCfg.BurstObservatory},
 		{"burstObservatory", oldCfg.BurstObservatory, newCfg.BurstObservatory},
 		{"metrics", oldCfg.Metrics, newCfg.Metrics},
 		{"metrics", oldCfg.Metrics, newCfg.Metrics},
 		{"geodata", oldCfg.Geodata, newCfg.Geodata},
 		{"geodata", oldCfg.Geodata, newCfg.Geodata},
+		{"env", oldCfg.Env, newCfg.Env},
 	}
 	}
 	for _, section := range static {
 	for _, section := range static {
 		if !rawEqualNormalized(section.old, section.new) {
 		if !rawEqualNormalized(section.old, section.new) {

+ 6 - 0
internal/xray/hot_diff_test.go

@@ -143,6 +143,12 @@ func TestComputeHotDiff_StaticSectionChangeNeedsRestart(t *testing.T) {
 	if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
 	if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
 		t.Fatal("observatory change must force a restart")
 		t.Fatal("observatory change must force a restart")
 	}
 	}
+
+	newCfg = makeHotConfig()
+	newCfg.Env = json_util.RawMessage(`{"XRAY_DNS_PATH":"/tmp/dns"}`)
+	if _, ok := ComputeHotDiff(makeHotConfig(), newCfg); ok {
+		t.Fatal("env change must force a restart: env vars are read only at process start")
+	}
 }
 }
 
 
 func TestComputeHotDiff_InboundAddRemoveChange(t *testing.T) {
 func TestComputeHotDiff_InboundAddRemoveChange(t *testing.T) {

+ 42 - 0
internal/xray/outbound_validation_test.go

@@ -0,0 +1,42 @@
+package xray
+
+import (
+	"strings"
+	"testing"
+)
+
+// TestValidateOutboundConfig_RejectsUnencryptedPublicVless covers xray-core
+// v26.7.11's refusal to build an unencrypted vless outbound to a public
+// address — the check now runs in-process, so the panel can surface it before
+// a config reaches the core and bricks startup. A private-address outbound and
+// a TLS outbound stay valid.
+func TestValidateOutboundConfig_RejectsUnencryptedPublicVless(t *testing.T) {
+	publicPlaintext := `{
+		"protocol": "vless",
+		"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
+		"streamSettings": {"network": "tcp", "security": "none"}
+	}`
+	if err := ValidateOutboundConfig([]byte(publicPlaintext)); err == nil {
+		t.Fatal("expected a public unencrypted vless outbound to be rejected")
+	} else if !strings.Contains(err.Error(), "prohibited") {
+		t.Fatalf("expected a prohibition error, got: %v", err)
+	}
+
+	privatePlaintext := `{
+		"protocol": "vless",
+		"settings": {"address": "10.0.0.1", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
+		"streamSettings": {"network": "tcp", "security": "none"}
+	}`
+	if err := ValidateOutboundConfig([]byte(privatePlaintext)); err != nil {
+		t.Fatalf("a private-address plaintext vless outbound must stay valid, got: %v", err)
+	}
+
+	publicTLS := `{
+		"protocol": "vless",
+		"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
+		"streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {"serverName": "example.com"}}
+	}`
+	if err := ValidateOutboundConfig([]byte(publicTLS)); err != nil {
+		t.Fatalf("a TLS-secured public vless outbound must stay valid, got: %v", err)
+	}
+}