瀏覽代碼

fix(sub): randomize fresh panel subscription paths (#6375)

* fix(sub): randomize fresh panel subscription paths

Seed distinct cryptographically random paths for base64, JSON, and Clash subscriptions when a panel database is first created. Persist them so restarts keep published URLs stable while upgrades preserve existing settings.

Generated-by: OpenCode:gpt-5.6-sol

* fix(sub): regenerate paths on settings reset

Keep subscription paths unpredictable after a factory reset, close the test database on failure, and update the builder, OpenAPI, and localized docs to describe panel-specific paths instead of obsolete fixed defaults.

Generated-by: OpenCode:gpt-5.6-sol
ilyusha 9 小時之前
父節點
當前提交
f9898e0b24

+ 4 - 4
docs/components/tools/subscription-builder.tsx

@@ -69,8 +69,8 @@ export function SubscriptionBuilder() {
   const [scheme, setScheme] = useState<'http' | 'https'>('https');
   const [host, setHost] = useState('sub.example.com');
   const [port, setPort] = useState('2096');
-  const [subPath, setSubPath] = useState('/sub/');
-  const [jsonPath, setJsonPath] = useState('/json/');
+  const [subPath, setSubPath] = useState('/your-sub-path/');
+  const [jsonPath, setJsonPath] = useState('/your-json-path/');
   const [subId, setSubId] = useState('user-1');
   const [behindProxy, setBehindProxy] = useState(false);
   const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
@@ -95,8 +95,8 @@ export function SubscriptionBuilder() {
     setScheme('https');
     setHost('sub.example.com');
     setPort('2096');
-    setSubPath('/sub/');
-    setJsonPath('/json/');
+    setSubPath('/your-sub-path/');
+    setJsonPath('/your-json-path/');
     setSubId('user-1');
     setBehindProxy(false);
     setClients(DEFAULT_CLIENTS);

+ 6 - 6
docs/content/docs/en/config/subscription.mdx

@@ -18,7 +18,7 @@ panel's subscription settings:
 | ------------- | ------- | --------------------------------------------------------------- |
 | `subPort`     | `2096`  | Listen port (separate from the panel).                          |
 | `subListen`   | _(all)_ | Bind address.                                                   |
-| `subPath`     | `/sub/` | Base path for raw subscription URLs.                            |
+| `subPath`     | _(random per panel)_ | Base path for raw subscription URLs.                 |
 | `subDomain`   | _(none)_| Public host; if set, the server only answers for that Host.     |
 | `subCertFile` / `subKeyFile` | _(none)_ | TLS cert + key — when set, the server serves **HTTPS**. |
 | `subEncrypt`  | `true`  | Base64-encode the raw subscription body.                        |
@@ -27,7 +27,7 @@ panel's subscription settings:
 A subscription URL looks like:
 
 ```text
-https://<sub-host>:<sub-port>/sub/<sub-id>
+https://<sub-host>:<sub-port>/<sub-path>/<sub-id>
 ```
 
 where `<sub-id>` is the client's **Sub ID**.
@@ -44,12 +44,12 @@ The **format is chosen by path**, each with its own enable toggle:
 
 | Format                | Path      | Enabled by       | Output                                              |
 | --------------------- | --------- | ---------------- | --------------------------------------------------- |
-| **Raw links**         | `/sub/`   | always (if on)   | A list of `vless://`, `vmess://`, … links (base64-encoded when `subEncrypt` is on). |
-| **JSON**              | `/json/`  | `subJsonEnable`  | Full Xray client config(s).                         |
-| **Clash / Mihomo**    | `/clash/` | `subClashEnable` | YAML profile.                                       |
+| **Raw links**         | `subPath`      | always (if on)   | A list of `vless://`, `vmess://`, … links (base64-encoded when `subEncrypt` is on). |
+| **JSON**              | `subJsonPath`  | `subJsonEnable`  | Full Xray client config(s).                         |
+| **Clash / Mihomo**    | `subClashPath` | `subClashEnable` | YAML profile.                                       |
 
 Only enabled inbounds using **VLESS, VMess, Trojan, Shadowsocks, or Hysteria2**
-appear in a subscription, ordered by their sub-sort index. Requesting `/sub/`
+appear in a subscription, ordered by their sub-sort index. Requesting `subPath`
 with an `Accept: text/html` header (or `?html=1`) returns a human-readable info
 page instead of the raw body.
 

+ 10 - 8
docs/content/docs/en/reference/api/clients.mdx

@@ -264,10 +264,11 @@ _openapi:
     - depth: 2
       title: Return every protocol URL (vless://, vmess://, trojan://, ss://,
         hysteria://, hy2://) for clients matching the subscription ID. Same
-        result set as /sub/<subId>, but as a JSON array — no base64. When an
-        inbound has streamSettings.externalProxy set, one URL is emitted per
-        external proxy. Empty array when the subId has no enabled clients.
-      url: '#return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients'
+        result set as the configured subPath endpoint, but as a JSON array — no
+        base64. When an inbound has streamSettings.externalProxy set, one URL is
+        emitted per external proxy. Empty array when the subId has no enabled
+        clients.
+      url: '#return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-the-configured-subpath-endpoint-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients'
     - depth: 2
       title: 'Return every URL for one client across all attached inbounds — the same
         strings the Copy URL button copies in the panel UI. Supported protocols:
@@ -496,10 +497,11 @@ _openapi:
         id: traffic-counters-for-a-client-identified-by-email
       - content: Return every protocol URL (vless://, vmess://, trojan://, ss://,
           hysteria://, hy2://) for clients matching the subscription ID. Same
-          result set as /sub/<subId>, but as a JSON array — no base64. When an
-          inbound has streamSettings.externalProxy set, one URL is emitted per
-          external proxy. Empty array when the subId has no enabled clients.
-        id: return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
+          result set as the configured subPath endpoint, but as a JSON array —
+          no base64. When an inbound has streamSettings.externalProxy set, one
+          URL is emitted per external proxy. Empty array when the subId has no
+          enabled clients.
+        id: return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-the-configured-subpath-endpoint-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
       - content: 'Return every URL for one client across all attached inbounds — the
           same strings the Copy URL button copies in the panel UI. Supported
           protocols: vmess, vless, trojan, shadowsocks, hysteria. If

+ 25 - 22
docs/content/docs/en/reference/api/subscription-server.mdx

@@ -2,9 +2,10 @@
 title: Subscription Server
 description: A separate HTTP/HTTPS server that serves proxy subscription links
   (standard, JSON, and Clash) to clients. The server listens on its own port
-  (default 10882) and is configured in Settings → Subscription. Paths are
-  configurable; defaults are shown below. All subscription endpoints set
-  response headers for client apps to read traffic/expiry info.
+  (default 2096) and is configured in Settings → Subscription. Fresh panels
+  generate random path prefixes for each format; all paths remain configurable.
+  Every subscription endpoint sets response headers for client apps to read
+  traffic/expiry info.
 full: true
 _openapi:
   preload:
@@ -15,34 +16,36 @@ _openapi:
         matching the subscription ID. When the request has an Accept: text/html
         header or ?html=1, renders a styled info page instead. With
         ?format=info, returns the page view-model as JSON (traffic, expiry,
-        online status; no links) for live polling. Default path: /sub/:subid.'
-      url: '#return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-default-path-subsubid'
+        online status; no links) for live polling. The path prefix is configured
+        by subPath.'
+      url: '#return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath'
     - depth: 2
-      title: 'Return subscription as a JSON array of proxy configs (one per enabled
-        client). Only when JSON subscription is enabled in settings. Default
-        path: /json/:subid.'
-      url: '#return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid'
+      title: Return subscription as a JSON array of proxy configs (one per enabled
+        client). Only when JSON subscription is enabled in settings. The path
+        prefix is configured by subJsonPath.
+      url: '#return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath'
     - depth: 2
-      title: 'Return subscription as a Clash/Mihomo-compatible YAML config, including
+      title: Return subscription as a Clash/Mihomo-compatible YAML config, including
         configured global Clash routing rules. Only when Clash subscription is
-        enabled in settings. Default path: /clash/:subid.'
-      url: '#return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid'
+        enabled in settings. The path prefix is configured by subClashPath.
+      url: '#return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath'
   structuredData:
     headings:
       - content: 'Return base64-encoded subscription links for all enabled clients
           matching the subscription ID. When the request has an Accept:
           text/html header or ?html=1, renders a styled info page instead. With
           ?format=info, returns the page view-model as JSON (traffic, expiry,
-          online status; no links) for live polling. Default path: /sub/:subid.'
-        id: return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-default-path-subsubid
-      - content: 'Return subscription as a JSON array of proxy configs (one per enabled
-          client). Only when JSON subscription is enabled in settings. Default
-          path: /json/:subid.'
-        id: return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
-      - content: 'Return subscription as a Clash/Mihomo-compatible YAML config,
-          including configured global Clash routing rules. Only when Clash
-          subscription is enabled in settings. Default path: /clash/:subid.'
-        id: return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+          online status; no links) for live polling. The path prefix is
+          configured by subPath.'
+        id: return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath
+      - content: Return subscription as a JSON array of proxy configs (one per enabled
+          client). Only when JSON subscription is enabled in settings. The path
+          prefix is configured by subJsonPath.
+        id: return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath
+      - content: Return subscription as a Clash/Mihomo-compatible YAML config, including
+          configured global Clash routing rules. Only when Clash subscription is
+          enabled in settings. The path prefix is configured by subClashPath.
+        id: return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath
     contents: []
 ---
 

+ 6 - 6
docs/content/docs/fa/config/subscription.mdx

@@ -18,7 +18,7 @@ icon: Rss
 | ------------- | ------- | --------------------------------------------------------------- |
 | `subPort`     | `2096`  | پورت گوش‌دادن (جدا از پنل).                                      |
 | `subListen`   | _(همه)_ | آدرس اتصال (bind).                                              |
-| `subPath`     | `/sub/` | مسیر پایه برای URLهای خام اشتراک.                                |
+| `subPath`     | _(تصادفی برای هر پنل)_ | مسیر پایه برای URLهای خام اشتراک.                  |
 | `subDomain`   | _(هیچ)_ | میزبان عمومی؛ اگر تنظیم شود، سرور فقط به همان Host پاسخ می‌دهد.   |
 | `subCertFile` / `subKeyFile` | _(هیچ)_ | گواهی و کلید TLS — هنگام تنظیم، سرور **HTTPS** ارائه می‌دهد. |
 | `subEncrypt`  | `true`  | بدنه‌ی خام اشتراک را با base64 رمزگذاری می‌کند.                   |
@@ -27,7 +27,7 @@ icon: Rss
 یک URL اشتراک به این شکل است:
 
 ```text
-https://<sub-host>:<sub-port>/sub/<sub-id>
+https://<sub-host>:<sub-port>/<sub-path>/<sub-id>
 ```
 
 که در آن `<sub-id>` همان **Sub ID** کلاینت است.
@@ -44,13 +44,13 @@ https://<sub-host>:<sub-port>/sub/<sub-id>
 
 | Format                | Path      | Enabled by       | Output                                              |
 | --------------------- | --------- | ---------------- | --------------------------------------------------- |
-| **لینک‌های خام**       | `/sub/`   | همیشه (اگر روشن باشد) | فهرستی از لینک‌های `vless://`، `vmess://`، … (هنگام فعال‌بودن `subEncrypt` با base64 رمزگذاری می‌شود). |
-| **JSON**              | `/json/`  | `subJsonEnable`  | پیکربندی(های) کامل کلاینت Xray.                      |
-| **Clash / Mihomo**    | `/clash/` | `subClashEnable` | پروفایل YAML.                                        |
+| **لینک‌های خام**       | `subPath`      | همیشه (اگر روشن باشد) | فهرستی از لینک‌های `vless://`، `vmess://`، … (هنگام فعال‌بودن `subEncrypt` با base64 رمزگذاری می‌شود). |
+| **JSON**              | `subJsonPath`  | `subJsonEnable`  | پیکربندی(های) کامل کلاینت Xray.                      |
+| **Clash / Mihomo**    | `subClashPath` | `subClashEnable` | پروفایل YAML.                                        |
 
 فقط ورودی‌های فعالی که از **VLESS، VMess، Trojan، Shadowsocks یا Hysteria2**
 استفاده می‌کنند در یک اشتراک ظاهر می‌شوند و بر اساس شاخص sub-sort آن‌ها مرتب می‌شوند.
-درخواست `/sub/` همراه با هدر `Accept: text/html` (یا `?html=1`) به‌جای بدنه‌ی خام،
+درخواست `subPath` همراه با هدر `Accept: text/html` (یا `?html=1`) به‌جای بدنه‌ی خام،
 یک صفحه‌ی اطلاعات خوانا برای انسان برمی‌گرداند.
 
 ### Base64 vs JSON

File diff suppressed because it is too large
+ 10 - 8
docs/content/docs/fa/reference/api/clients.mdx


+ 22 - 21
docs/content/docs/fa/reference/api/subscription-server.mdx

@@ -3,10 +3,10 @@ title: سرور اشتراک
 description: >-
   یک سرور HTTP/HTTPS جداگانه که لینک‌های اشتراک پراکسی (استاندارد، JSON و Clash)
   را به کلاینت‌ها ارائه می‌دهد. این سرور روی پورت اختصاصی خودش (به‌صورت پیش‌فرض
-  10882) گوش می‌دهد و در بخش Settings ← Subscription پیکربندی می‌شود. مسیرها قابل
-  پیکربندی هستند؛ مقادیر پیش‌فرض در ادامه نشان داده شده‌اند. همه‌ی نقاط پایانی
-  اشتراک، هدرهای پاسخ را برای خواندن اطلاعات ترافیک/انقضا توسط برنامه‌های کلاینت
-  تنظیم می‌کنند.
+  2096) گوش می‌دهد و در بخش Settings ← Subscription پیکربندی می‌شود. پنل‌های جدید
+  برای هر قالب پیشوند مسیر تصادفی تولید می‌کنند و همه‌ی مسیرها قابل پیکربندی
+  می‌مانند. همه‌ی نقاط پایانی اشتراک، هدرهای پاسخ را برای خواندن اطلاعات
+  ترافیک/انقضا توسط برنامه‌های کلاینت تنظیم می‌کنند.
 full: true
 _openapi:
   preload:
@@ -16,45 +16,46 @@ _openapi:
       title: >-
         Return base64-encoded subscription links for all enabled clients
         matching the subscription ID. When the request has an Accept: text/html
-        header or ?html=1, renders a styled info page instead. Default path:
-        /sub/:subid.
+        header or ?html=1, renders a styled info page instead. The path prefix is
+        configured by subPath.
       url: >-
-        #return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+        #return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath
     - depth: 2
       title: >-
         Return subscription as a JSON array of proxy configs (one per enabled
-        client). Only when JSON subscription is enabled in settings. Default
-        path: /json/:subid.
+        client). Only when JSON subscription is enabled in settings. The path
+        prefix is configured by subJsonPath.
       url: >-
-        #return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+        #return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath
     - depth: 2
       title: >-
         Return subscription as a Clash/Mihomo-compatible YAML config, including
         configured global Clash routing rules. Only when Clash subscription is
-        enabled in settings. Default path: /clash/:subid.
+        enabled in settings. The path prefix is configured by subClashPath.
       url: >-
-        #return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+        #return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath
   structuredData:
     headings:
       - content: >-
           Return base64-encoded subscription links for all enabled clients
           matching the subscription ID. When the request has an Accept:
-          text/html header or ?html=1, renders a styled info page instead.
-          Default path: /sub/:subid.
+          text/html header or ?html=1, renders a styled info page instead. The
+          path prefix is configured by subPath.
         id: >-
-          return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+          return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath
       - content: >-
           Return subscription as a JSON array of proxy configs (one per enabled
-          client). Only when JSON subscription is enabled in settings. Default
-          path: /json/:subid.
+          client). Only when JSON subscription is enabled in settings. The path
+          prefix is configured by subJsonPath.
         id: >-
-          return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+          return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath
       - content: >-
           Return subscription as a Clash/Mihomo-compatible YAML config,
           including configured global Clash routing rules. Only when Clash
-          subscription is enabled in settings. Default path: /clash/:subid.
+          subscription is enabled in settings. The path prefix is configured by
+          subClashPath.
         id: >-
-          return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+          return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath
     contents: []
 ---
 
@@ -70,4 +71,4 @@ export default function Layout(props) {
       <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/{subPath}{subid}","method":"get"},{"path":"/{jsonPath}{subid}","method":"get"},{"path":"/{clashPath}{subid}","method":"get"}]} showTitle />
     </>
   );
-}
+}

+ 6 - 6
docs/content/docs/ru/config/subscription.mdx

@@ -18,7 +18,7 @@ icon: Rss
 | ------------- | ------- | --------------------------------------------------------------- |
 | `subPort`     | `2096`  | Порт прослушивания (отдельный от панели).                       |
 | `subListen`   | _(все)_ | Адрес привязки.                                                 |
-| `subPath`     | `/sub/` | Базовый путь для необработанных URL подписок.                   |
+| `subPath`     | _(случайный для каждой панели)_ | Базовый путь для необработанных URL подписок. |
 | `subDomain`   | _(нет)_ | Публичный хост; если задан, сервер отвечает только для этого Host. |
 | `subCertFile` / `subKeyFile` | _(нет)_ | Сертификат + ключ TLS — когда заданы, сервер работает по **HTTPS**. |
 | `subEncrypt`  | `true`  | Кодировать тело необработанной подписки в base64.               |
@@ -27,7 +27,7 @@ icon: Rss
 URL подписки выглядит так:
 
 ```text
-https://<sub-host>:<sub-port>/sub/<sub-id>
+https://<sub-host>:<sub-port>/<sub-path>/<sub-id>
 ```
 
 где `<sub-id>` — это **Sub ID** клиента.
@@ -44,13 +44,13 @@ https://<sub-host>:<sub-port>/sub/<sub-id>
 
 | Формат                | Путь      | Включается       | Вывод                                               |
 | --------------------- | --------- | ---------------- | --------------------------------------------------- |
-| **Необработанные ссылки** | `/sub/`   | всегда (если включён) | Список ссылок `vless://`, `vmess://`, … (закодированных в base64, когда включён `subEncrypt`). |
-| **JSON**              | `/json/`  | `subJsonEnable`  | Полные клиентские конфигурации Xray.                |
-| **Clash / Mihomo**    | `/clash/` | `subClashEnable` | YAML-профиль.                                       |
+| **Необработанные ссылки** | `subPath`      | всегда (если включён) | Список ссылок `vless://`, `vmess://`, … (закодированных в base64, когда включён `subEncrypt`). |
+| **JSON**              | `subJsonPath`  | `subJsonEnable`  | Полные клиентские конфигурации Xray.                |
+| **Clash / Mihomo**    | `subClashPath` | `subClashEnable` | YAML-профиль.                                       |
 
 В подписке появляются только включённые входящие соединения, использующие
 **VLESS, VMess, Trojan, Shadowsocks или Hysteria2**, упорядоченные по их индексу
-сортировки подписки. Запрос `/sub/` с заголовком `Accept: text/html` (или
+сортировки подписки. Запрос `subPath` с заголовком `Accept: text/html` (или
 `?html=1`) возвращает удобочитаемую информационную страницу вместо
 необработанного тела.
 

File diff suppressed because it is too large
+ 10 - 8
docs/content/docs/ru/reference/api/clients.mdx


+ 22 - 21
docs/content/docs/ru/reference/api/subscription-server.mdx

@@ -3,10 +3,10 @@ title: Сервер подписок
 description: >-
   Отдельный HTTP/HTTPS-сервер, который отдаёт клиентам ссылки на подписки
   прокси (стандартные, JSON и Clash). Сервер слушает на собственном порту (по
-  умолчанию 10882) и настраивается в разделе Settings → Subscription. Пути
-  настраиваемы; значения по умолчанию показаны ниже. Все конечные точки подписок
-  устанавливают заголовки ответа, по которым клиентские приложения считывают
-  информацию о трафике и сроке действия.
+  умолчанию 2096) и настраивается в разделе Settings → Subscription. Новые
+  панели генерируют случайные префиксы путей для каждого формата; все пути можно
+  изменить. Все конечные точки подписок устанавливают заголовки ответа, по
+  которым клиентские приложения считывают информацию о трафике и сроке действия.
 full: true
 _openapi:
   preload:
@@ -16,45 +16,46 @@ _openapi:
       title: >-
         Return base64-encoded subscription links for all enabled clients
         matching the subscription ID. When the request has an Accept: text/html
-        header or ?html=1, renders a styled info page instead. Default path:
-        /sub/:subid.
+        header or ?html=1, renders a styled info page instead. The path prefix is
+        configured by subPath.
       url: >-
-        #return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+        #return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath
     - depth: 2
       title: >-
         Return subscription as a JSON array of proxy configs (one per enabled
-        client). Only when JSON subscription is enabled in settings. Default
-        path: /json/:subid.
+        client). Only when JSON subscription is enabled in settings. The path
+        prefix is configured by subJsonPath.
       url: >-
-        #return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+        #return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath
     - depth: 2
       title: >-
         Return subscription as a Clash/Mihomo-compatible YAML config, including
         configured global Clash routing rules. Only when Clash subscription is
-        enabled in settings. Default path: /clash/:subid.
+        enabled in settings. The path prefix is configured by subClashPath.
       url: >-
-        #return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+        #return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath
   structuredData:
     headings:
       - content: >-
           Return base64-encoded subscription links for all enabled clients
           matching the subscription ID. When the request has an Accept:
-          text/html header or ?html=1, renders a styled info page instead.
-          Default path: /sub/:subid.
+          text/html header or ?html=1, renders a styled info page instead. The
+          path prefix is configured by subPath.
         id: >-
-          return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+          return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath
       - content: >-
           Return subscription as a JSON array of proxy configs (one per enabled
-          client). Only when JSON subscription is enabled in settings. Default
-          path: /json/:subid.
+          client). Only when JSON subscription is enabled in settings. The path
+          prefix is configured by subJsonPath.
         id: >-
-          return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+          return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath
       - content: >-
           Return subscription as a Clash/Mihomo-compatible YAML config,
           including configured global Clash routing rules. Only when Clash
-          subscription is enabled in settings. Default path: /clash/:subid.
+          subscription is enabled in settings. The path prefix is configured by
+          subClashPath.
         id: >-
-          return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+          return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath
     contents: []
 ---
 
@@ -70,4 +71,4 @@ export default function Layout(props) {
       <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/{subPath}{subid}","method":"get"},{"path":"/{jsonPath}{subid}","method":"get"},{"path":"/{clashPath}{subid}","method":"get"}]} showTitle />
     </>
   );
-}
+}

+ 6 - 6
docs/content/docs/zh/config/subscription.mdx

@@ -14,7 +14,7 @@ icon: Rss
 | ------------- | ------- | --------------------------------------------------------------- |
 | `subPort`     | `2096`  | 监听端口(与面板分开)。                                        |
 | `subListen`   | _(全部)_ | 绑定地址。                                                      |
-| `subPath`     | `/sub/` | 原始订阅 URL 的基础路径。                                       |
+| `subPath`     | _(每个面板随机生成)_ | 原始订阅 URL 的基础路径。                         |
 | `subDomain`   | _(无)_  | 公开主机名;若设置,服务器仅响应该 Host。                       |
 | `subCertFile` / `subKeyFile` | _(无)_ | TLS 证书 + 密钥 —— 设置后,服务器以 **HTTPS** 提供服务。 |
 | `subEncrypt`  | `true`  | 对原始订阅内容进行 base64 编码。                               |
@@ -23,7 +23,7 @@ icon: Rss
 一个订阅 URL 形如:
 
 ```text
-https://<sub-host>:<sub-port>/sub/<sub-id>
+https://<sub-host>:<sub-port>/<sub-path>/<sub-id>
 ```
 
 其中 `<sub-id>` 是客户端的 **Sub ID**。
@@ -38,11 +38,11 @@ https://<sub-host>:<sub-port>/sub/<sub-id>
 
 | 格式                  | 路径      | 启用方式         | 输出                                                |
 | --------------------- | --------- | ---------------- | --------------------------------------------------- |
-| **原始链接**          | `/sub/`   | 始终(若已开启) | 一组 `vless://`、`vmess://` 等链接的列表(当 `subEncrypt` 开启时进行 base64 编码)。 |
-| **JSON**              | `/json/`  | `subJsonEnable`  | 完整的 Xray 客户端配置。                            |
-| **Clash / Mihomo**    | `/clash/` | `subClashEnable` | YAML 配置文件。                                     |
+| **原始链接**          | `subPath`      | 始终(若已开启) | 一组 `vless://`、`vmess://` 等链接的列表(当 `subEncrypt` 开启时进行 base64 编码)。 |
+| **JSON**              | `subJsonPath`  | `subJsonEnable`  | 完整的 Xray 客户端配置。                            |
+| **Clash / Mihomo**    | `subClashPath` | `subClashEnable` | YAML 配置文件。                                     |
 
-只有使用 **VLESS、VMess、Trojan、Shadowsocks 或 Hysteria2** 的已启用入站才会出现在订阅中,并按其订阅排序索引排列。使用 `Accept: text/html` 头(或 `?html=1`)请求 `/sub/` 会返回一个人类可读的信息页面,而非原始内容。
+只有使用 **VLESS、VMess、Trojan、Shadowsocks 或 Hysteria2** 的已启用入站才会出现在订阅中,并按其订阅排序索引排列。使用 `Accept: text/html` 头(或 `?html=1`)请求 `subPath` 会返回一个人类可读的信息页面,而非原始内容。
 
 ### Base64 与 JSON
 

File diff suppressed because it is too large
+ 4 - 4
docs/content/docs/zh/reference/api/clients.mdx


+ 14 - 14
docs/content/docs/zh/reference/api/subscription-server.mdx

@@ -2,7 +2,7 @@
 title: 订阅服务器
 description: >-
   一个独立的 HTTP/HTTPS 服务器,用于向客户端提供代理订阅链接(标准、JSON 和 Clash)。该服务器监听自己的端口(默认
-  10882),并在“设置 → 订阅”中进行配置。路径可自定义;下方展示的是默认值。所有订阅端点都会设置响应头,供客户端应用读取流量/到期信息。
+  2096),并在“设置 → 订阅”中进行配置。新面板会为每种格式生成随机路径前缀,所有路径仍可自定义。所有订阅端点都会设置响应头,供客户端应用读取流量/到期信息。
 full: true
 _openapi:
   preload:
@@ -11,36 +11,36 @@ _openapi:
     - depth: 2
       title: >-
         返回与该订阅 ID 匹配的所有已启用客户端的 base64 编码订阅链接。当请求带有 Accept: text/html
-        头或 ?html=1 时,改为渲染一个带样式的信息页面。默认路径:/sub/:subid
+        头或 ?html=1 时,改为渲染一个带样式的信息页面。路径前缀由 subPath 配置
       url: >-
-        #return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+        #return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath
     - depth: 2
       title: >-
-        以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。默认路径:/json/:subid
+        以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。路径前缀由 subJsonPath 配置
       url: >-
-        #return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+        #return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath
     - depth: 2
       title: >-
         以兼容 Clash/Mihomo 的 YAML 配置形式返回订阅,其中包含已配置的全局 Clash 路由规则。仅在设置中启用 Clash
-        订阅时可用。默认路径:/clash/:subid
+        订阅时可用。路径前缀由 subClashPath 配置
       url: >-
-        #return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+        #return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath
   structuredData:
     headings:
       - content: >-
           返回与该订阅 ID 匹配的所有已启用客户端的 base64 编码订阅链接。当请求带有 Accept:
-          text/html 头或 ?html=1 时,改为渲染一个带样式的信息页面。默认路径:/sub/:subid
+          text/html 头或 ?html=1 时,改为渲染一个带样式的信息页面。路径前缀由 subPath 配置
         id: >-
-          return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
+          return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-the-path-prefix-is-configured-by-subpath
       - content: >-
-          以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。默认路径:/json/:subid
+          以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。路径前缀由 subJsonPath 配置
         id: >-
-          return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
+          return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subjsonpath
       - content: >-
           以兼容 Clash/Mihomo 的 YAML 配置形式返回订阅,其中包含已配置的全局 Clash 路由规则。仅在设置中启用 Clash
-          订阅时可用。默认路径:/clash/:subid
+          订阅时可用。路径前缀由 subClashPath 配置
         id: >-
-          return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
+          return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-the-path-prefix-is-configured-by-subclashpath
     contents: []
 ---
 
@@ -56,4 +56,4 @@ export default function Layout(props) {
       <Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/{subPath}{subid}","method":"get"},{"path":"/{jsonPath}{subid}","method":"get"},{"path":"/{clashPath}{subid}","method":"get"}]} showTitle />
     </>
   );
-}
+}

+ 5 - 5
docs/public/openapi.json

@@ -3512,7 +3512,7 @@
     },
     {
       "name": "Subscription Server",
-      "description": "A separate HTTP/HTTPS server that serves proxy subscription links (standard, JSON, and Clash) to clients. The server listens on its own port (default 10882) and is configured in Settings → Subscription. Paths are configurable; defaults are shown below. All subscription endpoints set response headers for client apps to read traffic/expiry info."
+      "description": "A separate HTTP/HTTPS server that serves proxy subscription links (standard, JSON, and Clash) to clients. The server listens on its own port (default 2096) and is configured in Settings → Subscription. Fresh panels generate random path prefixes for each format; all paths remain configurable. Every subscription endpoint sets response headers for client apps to read traffic/expiry info."
     },
     {
       "name": "WebSocket",
@@ -8731,7 +8731,7 @@
         "tags": [
           "Clients"
         ],
-        "summary": "Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as /sub/<subId>, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.",
+        "summary": "Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as the configured subPath endpoint, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.",
         "operationId": "get_panel_api_clients_subLinks_subId",
         "parameters": [
           {
@@ -12639,7 +12639,7 @@
         "tags": [
           "Subscription Server"
         ],
-        "summary": "Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. Default path: /sub/:subid.",
+        "summary": "Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. The path prefix is configured by subPath.",
         "operationId": "get_subPath_subid",
         "parameters": [
           {
@@ -12698,7 +12698,7 @@
         "tags": [
           "Subscription Server"
         ],
-        "summary": "Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. Default path: /json/:subid.",
+        "summary": "Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. The path prefix is configured by subJsonPath.",
         "operationId": "get_jsonPath_subid",
         "parameters": [
           {
@@ -12748,7 +12748,7 @@
         "tags": [
           "Subscription Server"
         ],
-        "summary": "Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. Default path: /clash/:subid.",
+        "summary": "Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. The path prefix is configured by subClashPath.",
         "operationId": "get_clashPath_subid",
         "parameters": [
           {

+ 5 - 5
frontend/public/openapi.json

@@ -3512,7 +3512,7 @@
     },
     {
       "name": "Subscription Server",
-      "description": "A separate HTTP/HTTPS server that serves proxy subscription links (standard, JSON, and Clash) to clients. The server listens on its own port (default 10882) and is configured in Settings → Subscription. Paths are configurable; defaults are shown below. All subscription endpoints set response headers for client apps to read traffic/expiry info."
+      "description": "A separate HTTP/HTTPS server that serves proxy subscription links (standard, JSON, and Clash) to clients. The server listens on its own port (default 2096) and is configured in Settings → Subscription. Fresh panels generate random path prefixes for each format; all paths remain configurable. Every subscription endpoint sets response headers for client apps to read traffic/expiry info."
     },
     {
       "name": "WebSocket",
@@ -8731,7 +8731,7 @@
         "tags": [
           "Clients"
         ],
-        "summary": "Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as /sub/<subId>, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.",
+        "summary": "Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as the configured subPath endpoint, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.",
         "operationId": "get_panel_api_clients_subLinks_subId",
         "parameters": [
           {
@@ -12639,7 +12639,7 @@
         "tags": [
           "Subscription Server"
         ],
-        "summary": "Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. Default path: /sub/:subid.",
+        "summary": "Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. The path prefix is configured by subPath.",
         "operationId": "get_subPath_subid",
         "parameters": [
           {
@@ -12698,7 +12698,7 @@
         "tags": [
           "Subscription Server"
         ],
-        "summary": "Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. Default path: /json/:subid.",
+        "summary": "Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. The path prefix is configured by subJsonPath.",
         "operationId": "get_jsonPath_subid",
         "parameters": [
           {
@@ -12748,7 +12748,7 @@
         "tags": [
           "Subscription Server"
         ],
-        "summary": "Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. Default path: /clash/:subid.",
+        "summary": "Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. The path prefix is configured by subClashPath.",
         "operationId": "get_clashPath_subid",
         "parameters": [
           {

+ 5 - 5
frontend/src/pages/api-docs/endpoints.ts

@@ -1279,7 +1279,7 @@ export const sections: readonly Section[] = [
         method: 'GET',
         path: '/panel/api/clients/subLinks/:subId',
         summary:
-          'Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as /sub/<subId>, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.',
+          'Return every protocol URL (vless://, vmess://, trojan://, ss://, hysteria://, hy2://) for clients matching the subscription ID. Same result set as the configured subPath endpoint, but as a JSON array — no base64. When an inbound has streamSettings.externalProxy set, one URL is emitted per external proxy. Empty array when the subId has no enabled clients.',
         params: [
           {
             name: 'subId',
@@ -2299,7 +2299,7 @@ export const sections: readonly Section[] = [
     id: 'subscription',
     title: 'Subscription Server',
     description:
-      'A separate HTTP/HTTPS server that serves proxy subscription links (standard, JSON, and Clash) to clients. The server listens on its own port (default 10882) and is configured in Settings → Subscription. Paths are configurable; defaults are shown below. All subscription endpoints set response headers for client apps to read traffic/expiry info.',
+      'A separate HTTP/HTTPS server that serves proxy subscription links (standard, JSON, and Clash) to clients. The server listens on its own port (default 2096) and is configured in Settings → Subscription. Fresh panels generate random path prefixes for each format; all paths remain configurable. Every subscription endpoint sets response headers for client apps to read traffic/expiry info.',
     subHeader: [
       {
         name: 'Subscription-Userinfo',
@@ -2327,7 +2327,7 @@ export const sections: readonly Section[] = [
         method: 'GET',
         path: '/{subPath}:subid',
         summary:
-          'Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. Default path: /sub/:subid.',
+          'Return base64-encoded subscription links for all enabled clients matching the subscription ID. When the request has an Accept: text/html header or ?html=1, renders a styled info page instead. With ?format=info, returns the page view-model as JSON (traffic, expiry, online status; no links) for live polling. The path prefix is configured by subPath.',
         params: [
           { name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' },
           {
@@ -2343,14 +2343,14 @@ export const sections: readonly Section[] = [
         method: 'GET',
         path: '/{jsonPath}:subid',
         summary:
-          'Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. Default path: /json/:subid.',
+          'Return subscription as a JSON array of proxy configs (one per enabled client). Only when JSON subscription is enabled in settings. The path prefix is configured by subJsonPath.',
         params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
       },
       {
         method: 'GET',
         path: '/{clashPath}:subid',
         summary:
-          'Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. Default path: /clash/:subid.',
+          'Return subscription as a Clash/Mihomo-compatible YAML config, including configured global Clash routing rules. Only when Clash subscription is enabled in settings. The path prefix is configured by subClashPath.',
         params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
       },
     ],

+ 21 - 0
internal/database/db.go

@@ -1169,6 +1169,22 @@ func initUser() error {
 	return nil
 }
 
+func seedRandomSubscriptionPaths() error {
+	settings := []model.Setting{
+		{Key: "subPath", Value: "/" + random.NumLower(16) + "/"},
+		{Key: "subJsonPath", Value: "/" + random.NumLower(16) + "/"},
+		{Key: "subClashPath", Value: "/" + random.NumLower(16) + "/"},
+	}
+	return db.Transaction(func(tx *gorm.DB) error {
+		for i := range settings {
+			if err := tx.Where("key = ?", settings[i].Key).FirstOrCreate(&settings[i]).Error; err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
 func runSeeders(isUsersEmpty bool) error {
 	empty, err := isTableEmpty("history_of_seeders")
 	if err != nil {
@@ -2138,6 +2154,11 @@ func InitDB(dbPath string) error {
 	if err != nil {
 		return err
 	}
+	if isUsersEmpty {
+		if err := seedRandomSubscriptionPaths(); err != nil {
+			return err
+		}
+	}
 
 	if err := initUser(); err != nil {
 		return err

+ 51 - 0
internal/database/db_seed_test.go

@@ -9,6 +9,54 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
+func TestInitDB_GeneratesPerPanelSubscriptionPaths(t *testing.T) {
+	pathPattern := regexp.MustCompile(`^/[0-9a-z]{16}/$`)
+	loadPaths := func(dbPath string) map[string]string {
+		t.Helper()
+		if err := InitDB(dbPath); err != nil {
+			t.Fatalf("InitDB failed: %v", err)
+		}
+		defer func() {
+			if err := CloseDB(); err != nil {
+				t.Errorf("CloseDB failed: %v", err)
+			}
+		}()
+
+		keys := []string{"subPath", "subJsonPath", "subClashPath"}
+		paths := make(map[string]string, len(keys))
+		for _, key := range keys {
+			var setting model.Setting
+			if err := db.Where("key = ?", key).First(&setting).Error; err != nil {
+				t.Fatalf("read %s: %v", key, err)
+			}
+			if !pathPattern.MatchString(setting.Value) {
+				t.Fatalf("%s = %q, want /<16 lowercase alphanumeric characters>/", key, setting.Value)
+			}
+			paths[key] = setting.Value
+		}
+		if paths["subPath"] == paths["subJsonPath"] || paths["subPath"] == paths["subClashPath"] || paths["subJsonPath"] == paths["subClashPath"] {
+			t.Fatalf("subscription paths must be distinct: %v", paths)
+		}
+		return paths
+	}
+
+	firstDB := filepath.Join(t.TempDir(), "x-ui.db")
+	first := loadPaths(firstDB)
+	reloaded := loadPaths(firstDB)
+	for key, firstPath := range first {
+		if firstPath != reloaded[key] {
+			t.Fatalf("%s changed after restart: %q, then %q", key, firstPath, reloaded[key])
+		}
+	}
+
+	second := loadPaths(filepath.Join(t.TempDir(), "x-ui.db"))
+	for key, firstPath := range first {
+		if firstPath == second[key] {
+			t.Fatalf("%s reused across panels: %q", key, firstPath)
+		}
+	}
+}
+
 func TestSeedClientsFromInboundJSON_IsIdempotentAgainstExistingClients(t *testing.T) {
 	dbDir := t.TempDir()
 	t.Setenv("XUI_DB_FOLDER", dbDir)
@@ -168,6 +216,9 @@ func TestNormalizeSettingPaths_RepairsLegacyValues(t *testing.T) {
 		{Key: "subClashPath", Value: "clash/"},
 		{Key: "webBasePath", Value: "/panel/"},
 	}
+	if err := db.Where("key IN ?", []string{"subPath", "subJsonPath", "subClashPath"}).Delete(&model.Setting{}).Error; err != nil {
+		t.Fatalf("clear generated subscription paths: %v", err)
+	}
 	for i := range seed {
 		if err := db.Create(&seed[i]).Error; err != nil {
 			t.Fatalf("seed setting %s: %v", seed[i].Key, err)

+ 11 - 6
internal/web/service/setting.go

@@ -309,12 +309,17 @@ func getEnv(key, fallback string) string {
 
 func (s *SettingService) ResetSettings() error {
 	db := database.GetDB()
-	err := db.Where("1 = 1").Delete(model.Setting{}).Error
-	if err != nil {
-		return err
-	}
-	return db.Model(model.User{}).
-		Where("1 = 1").Error
+	return db.Transaction(func(tx *gorm.DB) error {
+		if err := tx.Where("1 = 1").Delete(model.Setting{}).Error; err != nil {
+			return err
+		}
+		paths := []model.Setting{
+			{Key: "subPath", Value: "/" + random.NumLower(16) + "/"},
+			{Key: "subJsonPath", Value: "/" + random.NumLower(16) + "/"},
+			{Key: "subClashPath", Value: "/" + random.NumLower(16) + "/"},
+		}
+		return tx.Create(&paths).Error
+	})
 }
 
 func (s *SettingService) getSetting(key string) (*model.Setting, error) {

+ 39 - 0
internal/web/service/setting_security_test.go

@@ -2,6 +2,7 @@ package service
 
 import (
 	"path/filepath"
+	"regexp"
 	"testing"
 
 	"github.com/xlzd/gotp"
@@ -10,6 +11,44 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 )
 
+func TestResetSettingsRegeneratesSubscriptionPaths(t *testing.T) {
+	setupSettingTestDB(t)
+	s := &SettingService{}
+	for key, value := range map[string]string{
+		"subPath":      "/sub/",
+		"subJsonPath":  "/json/",
+		"subClashPath": "/clash/",
+		"webPort":      "8443",
+	} {
+		if err := s.saveSetting(key, value); err != nil {
+			t.Fatalf("save %s: %v", key, err)
+		}
+	}
+
+	if err := s.ResetSettings(); err != nil {
+		t.Fatalf("ResetSettings: %v", err)
+	}
+
+	pathPattern := regexp.MustCompile(`^/[0-9a-z]{16}/$`)
+	paths := map[string]string{}
+	for _, key := range []string{"subPath", "subJsonPath", "subClashPath"} {
+		value, err := s.getString(key)
+		if err != nil {
+			t.Fatalf("read %s: %v", key, err)
+		}
+		if !pathPattern.MatchString(value) {
+			t.Errorf("%s = %q, want /<16 lowercase alphanumeric characters>/", key, value)
+		}
+		paths[key] = value
+	}
+	if paths["subPath"] == paths["subJsonPath"] || paths["subPath"] == paths["subClashPath"] || paths["subJsonPath"] == paths["subClashPath"] {
+		t.Fatalf("subscription paths must be distinct: %v", paths)
+	}
+	if port, err := s.GetPort(); err != nil || port != 2053 {
+		t.Fatalf("web port after reset = %d, %v; want 2053", port, err)
+	}
+}
+
 func setupSettingTestDB(t *testing.T) {
 	t.Helper()
 	if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {

Some files were not shown because too many files changed in this diff