Browse Source

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 hours ago
parent
commit
f9898e0b24

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

@@ -69,8 +69,8 @@ export function SubscriptionBuilder() {
   const [scheme, setScheme] = useState<'http' | 'https'>('https');
   const [scheme, setScheme] = useState<'http' | 'https'>('https');
   const [host, setHost] = useState('sub.example.com');
   const [host, setHost] = useState('sub.example.com');
   const [port, setPort] = useState('2096');
   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 [subId, setSubId] = useState('user-1');
   const [behindProxy, setBehindProxy] = useState(false);
   const [behindProxy, setBehindProxy] = useState(false);
   const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
   const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
@@ -95,8 +95,8 @@ export function SubscriptionBuilder() {
     setScheme('https');
     setScheme('https');
     setHost('sub.example.com');
     setHost('sub.example.com');
     setPort('2096');
     setPort('2096');
-    setSubPath('/sub/');
-    setJsonPath('/json/');
+    setSubPath('/your-sub-path/');
+    setJsonPath('/your-json-path/');
     setSubId('user-1');
     setSubId('user-1');
     setBehindProxy(false);
     setBehindProxy(false);
     setClients(DEFAULT_CLIENTS);
     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).                          |
 | `subPort`     | `2096`  | Listen port (separate from the panel).                          |
 | `subListen`   | _(all)_ | Bind address.                                                   |
 | `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.     |
 | `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**. |
 | `subCertFile` / `subKeyFile` | _(none)_ | TLS cert + key — when set, the server serves **HTTPS**. |
 | `subEncrypt`  | `true`  | Base64-encode the raw subscription body.                        |
 | `subEncrypt`  | `true`  | Base64-encode the raw subscription body.                        |
@@ -27,7 +27,7 @@ panel's subscription settings:
 A subscription URL looks like:
 A subscription URL looks like:
 
 
 ```text
 ```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**.
 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                                              |
 | 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**
 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
 with an `Accept: text/html` header (or `?html=1`) returns a human-readable info
 page instead of the raw body.
 page instead of the raw body.
 
 

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

@@ -264,10 +264,11 @@ _openapi:
     - depth: 2
     - depth: 2
       title: Return every protocol URL (vless://, vmess://, trojan://, ss://,
       title: Return every protocol URL (vless://, vmess://, trojan://, ss://,
         hysteria://, hy2://) for clients matching the subscription ID. Same
         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
     - depth: 2
       title: 'Return every URL for one client across all attached inbounds — the same
       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:
         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
         id: traffic-counters-for-a-client-identified-by-email
       - content: Return every protocol URL (vless://, vmess://, trojan://, ss://,
       - content: Return every protocol URL (vless://, vmess://, trojan://, ss://,
           hysteria://, hy2://) for clients matching the subscription ID. Same
           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
       - content: 'Return every URL for one client across all attached inbounds — the
           same strings the Copy URL button copies in the panel UI. Supported
           same strings the Copy URL button copies in the panel UI. Supported
           protocols: vmess, vless, trojan, shadowsocks, hysteria. If
           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
 title: Subscription Server
 description: A separate HTTP/HTTPS server that serves proxy subscription links
 description: A separate HTTP/HTTPS server that serves proxy subscription links
   (standard, JSON, and Clash) to clients. The server listens on its own port
   (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
 full: true
 _openapi:
 _openapi:
   preload:
   preload:
@@ -15,34 +16,36 @@ _openapi:
         matching the subscription ID. When the request has an Accept: text/html
         matching the subscription ID. When the request has an Accept: text/html
         header or ?html=1, renders a styled info page instead. With
         header or ?html=1, renders a styled info page instead. With
         ?format=info, returns the page view-model as JSON (traffic, expiry,
         ?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
     - 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
     - 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
         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:
   structuredData:
     headings:
     headings:
       - content: 'Return base64-encoded subscription links for all enabled clients
       - content: 'Return base64-encoded subscription links for all enabled clients
           matching the subscription ID. When the request has an Accept:
           matching the subscription ID. When the request has an Accept:
           text/html header or ?html=1, renders a styled info page instead. With
           text/html header or ?html=1, renders a styled info page instead. With
           ?format=info, returns the page view-model as JSON (traffic, expiry,
           ?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: []
     contents: []
 ---
 ---
 
 

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

@@ -18,7 +18,7 @@ icon: Rss
 | ------------- | ------- | --------------------------------------------------------------- |
 | ------------- | ------- | --------------------------------------------------------------- |
 | `subPort`     | `2096`  | پورت گوش‌دادن (جدا از پنل).                                      |
 | `subPort`     | `2096`  | پورت گوش‌دادن (جدا از پنل).                                      |
 | `subListen`   | _(همه)_ | آدرس اتصال (bind).                                              |
 | `subListen`   | _(همه)_ | آدرس اتصال (bind).                                              |
-| `subPath`     | `/sub/` | مسیر پایه برای URLهای خام اشتراک.                                |
+| `subPath`     | _(تصادفی برای هر پنل)_ | مسیر پایه برای URLهای خام اشتراک.                  |
 | `subDomain`   | _(هیچ)_ | میزبان عمومی؛ اگر تنظیم شود، سرور فقط به همان Host پاسخ می‌دهد.   |
 | `subDomain`   | _(هیچ)_ | میزبان عمومی؛ اگر تنظیم شود، سرور فقط به همان Host پاسخ می‌دهد.   |
 | `subCertFile` / `subKeyFile` | _(هیچ)_ | گواهی و کلید TLS — هنگام تنظیم، سرور **HTTPS** ارائه می‌دهد. |
 | `subCertFile` / `subKeyFile` | _(هیچ)_ | گواهی و کلید TLS — هنگام تنظیم، سرور **HTTPS** ارائه می‌دهد. |
 | `subEncrypt`  | `true`  | بدنه‌ی خام اشتراک را با base64 رمزگذاری می‌کند.                   |
 | `subEncrypt`  | `true`  | بدنه‌ی خام اشتراک را با base64 رمزگذاری می‌کند.                   |
@@ -27,7 +27,7 @@ icon: Rss
 یک URL اشتراک به این شکل است:
 یک URL اشتراک به این شکل است:
 
 
 ```text
 ```text
-https://<sub-host>:<sub-port>/sub/<sub-id>
+https://<sub-host>:<sub-port>/<sub-path>/<sub-id>
 ```
 ```
 
 
 که در آن `<sub-id>` همان **Sub ID** کلاینت است.
 که در آن `<sub-id>` همان **Sub ID** کلاینت است.
@@ -44,13 +44,13 @@ https://<sub-host>:<sub-port>/sub/<sub-id>
 
 
 | Format                | Path      | Enabled by       | Output                                              |
 | 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**
 فقط ورودی‌های فعالی که از **VLESS، VMess، Trojan، Shadowsocks یا Hysteria2**
 استفاده می‌کنند در یک اشتراک ظاهر می‌شوند و بر اساس شاخص sub-sort آن‌ها مرتب می‌شوند.
 استفاده می‌کنند در یک اشتراک ظاهر می‌شوند و بر اساس شاخص sub-sort آن‌ها مرتب می‌شوند.
-درخواست `/sub/` همراه با هدر `Accept: text/html` (یا `?html=1`) به‌جای بدنه‌ی خام،
+درخواست `subPath` همراه با هدر `Accept: text/html` (یا `?html=1`) به‌جای بدنه‌ی خام،
 یک صفحه‌ی اطلاعات خوانا برای انسان برمی‌گرداند.
 یک صفحه‌ی اطلاعات خوانا برای انسان برمی‌گرداند.
 
 
 ### Base64 vs JSON
 ### 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: >-
 description: >-
   یک سرور HTTP/HTTPS جداگانه که لینک‌های اشتراک پراکسی (استاندارد، JSON و Clash)
   یک سرور HTTP/HTTPS جداگانه که لینک‌های اشتراک پراکسی (استاندارد، JSON و Clash)
   را به کلاینت‌ها ارائه می‌دهد. این سرور روی پورت اختصاصی خودش (به‌صورت پیش‌فرض
   را به کلاینت‌ها ارائه می‌دهد. این سرور روی پورت اختصاصی خودش (به‌صورت پیش‌فرض
-  10882) گوش می‌دهد و در بخش Settings ← Subscription پیکربندی می‌شود. مسیرها قابل
-  پیکربندی هستند؛ مقادیر پیش‌فرض در ادامه نشان داده شده‌اند. همه‌ی نقاط پایانی
-  اشتراک، هدرهای پاسخ را برای خواندن اطلاعات ترافیک/انقضا توسط برنامه‌های کلاینت
-  تنظیم می‌کنند.
+  2096) گوش می‌دهد و در بخش Settings ← Subscription پیکربندی می‌شود. پنل‌های جدید
+  برای هر قالب پیشوند مسیر تصادفی تولید می‌کنند و همه‌ی مسیرها قابل پیکربندی
+  می‌مانند. همه‌ی نقاط پایانی اشتراک، هدرهای پاسخ را برای خواندن اطلاعات
+  ترافیک/انقضا توسط برنامه‌های کلاینت تنظیم می‌کنند.
 full: true
 full: true
 _openapi:
 _openapi:
   preload:
   preload:
@@ -16,45 +16,46 @@ _openapi:
       title: >-
       title: >-
         Return base64-encoded subscription links for all enabled clients
         Return base64-encoded subscription links for all enabled clients
         matching the subscription ID. When the request has an Accept: text/html
         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: >-
       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
     - depth: 2
       title: >-
       title: >-
         Return subscription as a JSON array of proxy configs (one per enabled
         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: >-
       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
     - depth: 2
       title: >-
       title: >-
         Return subscription as a Clash/Mihomo-compatible YAML config, including
         Return subscription as a Clash/Mihomo-compatible YAML config, including
         configured global Clash routing rules. Only when Clash subscription is
         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: >-
       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:
   structuredData:
     headings:
     headings:
       - content: >-
       - content: >-
           Return base64-encoded subscription links for all enabled clients
           Return base64-encoded subscription links for all enabled clients
           matching the subscription ID. When the request has an Accept:
           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: >-
         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: >-
       - content: >-
           Return subscription as a JSON array of proxy configs (one per enabled
           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: >-
         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: >-
       - content: >-
           Return subscription as a Clash/Mihomo-compatible YAML config,
           Return subscription as a Clash/Mihomo-compatible YAML config,
           including configured global Clash routing rules. Only when Clash
           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: >-
         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: []
     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 />
       <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`  | Порт прослушивания (отдельный от панели).                       |
 | `subPort`     | `2096`  | Порт прослушивания (отдельный от панели).                       |
 | `subListen`   | _(все)_ | Адрес привязки.                                                 |
 | `subListen`   | _(все)_ | Адрес привязки.                                                 |
-| `subPath`     | `/sub/` | Базовый путь для необработанных URL подписок.                   |
+| `subPath`     | _(случайный для каждой панели)_ | Базовый путь для необработанных URL подписок. |
 | `subDomain`   | _(нет)_ | Публичный хост; если задан, сервер отвечает только для этого Host. |
 | `subDomain`   | _(нет)_ | Публичный хост; если задан, сервер отвечает только для этого Host. |
 | `subCertFile` / `subKeyFile` | _(нет)_ | Сертификат + ключ TLS — когда заданы, сервер работает по **HTTPS**. |
 | `subCertFile` / `subKeyFile` | _(нет)_ | Сертификат + ключ TLS — когда заданы, сервер работает по **HTTPS**. |
 | `subEncrypt`  | `true`  | Кодировать тело необработанной подписки в base64.               |
 | `subEncrypt`  | `true`  | Кодировать тело необработанной подписки в base64.               |
@@ -27,7 +27,7 @@ icon: Rss
 URL подписки выглядит так:
 URL подписки выглядит так:
 
 
 ```text
 ```text
-https://<sub-host>:<sub-port>/sub/<sub-id>
+https://<sub-host>:<sub-port>/<sub-path>/<sub-id>
 ```
 ```
 
 
 где `<sub-id>` — это **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**, упорядоченные по их индексу
 **VLESS, VMess, Trojan, Shadowsocks или Hysteria2**, упорядоченные по их индексу
-сортировки подписки. Запрос `/sub/` с заголовком `Accept: text/html` (или
+сортировки подписки. Запрос `subPath` с заголовком `Accept: text/html` (или
 `?html=1`) возвращает удобочитаемую информационную страницу вместо
 `?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: >-
 description: >-
   Отдельный HTTP/HTTPS-сервер, который отдаёт клиентам ссылки на подписки
   Отдельный HTTP/HTTPS-сервер, который отдаёт клиентам ссылки на подписки
   прокси (стандартные, JSON и Clash). Сервер слушает на собственном порту (по
   прокси (стандартные, JSON и Clash). Сервер слушает на собственном порту (по
-  умолчанию 10882) и настраивается в разделе Settings → Subscription. Пути
-  настраиваемы; значения по умолчанию показаны ниже. Все конечные точки подписок
-  устанавливают заголовки ответа, по которым клиентские приложения считывают
-  информацию о трафике и сроке действия.
+  умолчанию 2096) и настраивается в разделе Settings → Subscription. Новые
+  панели генерируют случайные префиксы путей для каждого формата; все пути можно
+  изменить. Все конечные точки подписок устанавливают заголовки ответа, по
+  которым клиентские приложения считывают информацию о трафике и сроке действия.
 full: true
 full: true
 _openapi:
 _openapi:
   preload:
   preload:
@@ -16,45 +16,46 @@ _openapi:
       title: >-
       title: >-
         Return base64-encoded subscription links for all enabled clients
         Return base64-encoded subscription links for all enabled clients
         matching the subscription ID. When the request has an Accept: text/html
         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: >-
       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
     - depth: 2
       title: >-
       title: >-
         Return subscription as a JSON array of proxy configs (one per enabled
         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: >-
       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
     - depth: 2
       title: >-
       title: >-
         Return subscription as a Clash/Mihomo-compatible YAML config, including
         Return subscription as a Clash/Mihomo-compatible YAML config, including
         configured global Clash routing rules. Only when Clash subscription is
         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: >-
       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:
   structuredData:
     headings:
     headings:
       - content: >-
       - content: >-
           Return base64-encoded subscription links for all enabled clients
           Return base64-encoded subscription links for all enabled clients
           matching the subscription ID. When the request has an Accept:
           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: >-
         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: >-
       - content: >-
           Return subscription as a JSON array of proxy configs (one per enabled
           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: >-
         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: >-
       - content: >-
           Return subscription as a Clash/Mihomo-compatible YAML config,
           Return subscription as a Clash/Mihomo-compatible YAML config,
           including configured global Clash routing rules. Only when Clash
           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: >-
         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: []
     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 />
       <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`  | 监听端口(与面板分开)。                                        |
 | `subPort`     | `2096`  | 监听端口(与面板分开)。                                        |
 | `subListen`   | _(全部)_ | 绑定地址。                                                      |
 | `subListen`   | _(全部)_ | 绑定地址。                                                      |
-| `subPath`     | `/sub/` | 原始订阅 URL 的基础路径。                                       |
+| `subPath`     | _(每个面板随机生成)_ | 原始订阅 URL 的基础路径。                         |
 | `subDomain`   | _(无)_  | 公开主机名;若设置,服务器仅响应该 Host。                       |
 | `subDomain`   | _(无)_  | 公开主机名;若设置,服务器仅响应该 Host。                       |
 | `subCertFile` / `subKeyFile` | _(无)_ | TLS 证书 + 密钥 —— 设置后,服务器以 **HTTPS** 提供服务。 |
 | `subCertFile` / `subKeyFile` | _(无)_ | TLS 证书 + 密钥 —— 设置后,服务器以 **HTTPS** 提供服务。 |
 | `subEncrypt`  | `true`  | 对原始订阅内容进行 base64 编码。                               |
 | `subEncrypt`  | `true`  | 对原始订阅内容进行 base64 编码。                               |
@@ -23,7 +23,7 @@ icon: Rss
 一个订阅 URL 形如:
 一个订阅 URL 形如:
 
 
 ```text
 ```text
-https://<sub-host>:<sub-port>/sub/<sub-id>
+https://<sub-host>:<sub-port>/<sub-path>/<sub-id>
 ```
 ```
 
 
 其中 `<sub-id>` 是客户端的 **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
 ### 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: 订阅服务器
 title: 订阅服务器
 description: >-
 description: >-
   一个独立的 HTTP/HTTPS 服务器,用于向客户端提供代理订阅链接(标准、JSON 和 Clash)。该服务器监听自己的端口(默认
   一个独立的 HTTP/HTTPS 服务器,用于向客户端提供代理订阅链接(标准、JSON 和 Clash)。该服务器监听自己的端口(默认
-  10882),并在“设置 → 订阅”中进行配置。路径可自定义;下方展示的是默认值。所有订阅端点都会设置响应头,供客户端应用读取流量/到期信息。
+  2096),并在“设置 → 订阅”中进行配置。新面板会为每种格式生成随机路径前缀,所有路径仍可自定义。所有订阅端点都会设置响应头,供客户端应用读取流量/到期信息。
 full: true
 full: true
 _openapi:
 _openapi:
   preload:
   preload:
@@ -11,36 +11,36 @@ _openapi:
     - depth: 2
     - depth: 2
       title: >-
       title: >-
         返回与该订阅 ID 匹配的所有已启用客户端的 base64 编码订阅链接。当请求带有 Accept: text/html
         返回与该订阅 ID 匹配的所有已启用客户端的 base64 编码订阅链接。当请求带有 Accept: text/html
-        头或 ?html=1 时,改为渲染一个带样式的信息页面。默认路径:/sub/:subid
+        头或 ?html=1 时,改为渲染一个带样式的信息页面。路径前缀由 subPath 配置
       url: >-
       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
     - depth: 2
       title: >-
       title: >-
-        以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。默认路径:/json/:subid
+        以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。路径前缀由 subJsonPath 配置
       url: >-
       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
     - depth: 2
       title: >-
       title: >-
         以兼容 Clash/Mihomo 的 YAML 配置形式返回订阅,其中包含已配置的全局 Clash 路由规则。仅在设置中启用 Clash
         以兼容 Clash/Mihomo 的 YAML 配置形式返回订阅,其中包含已配置的全局 Clash 路由规则。仅在设置中启用 Clash
-        订阅时可用。默认路径:/clash/:subid
+        订阅时可用。路径前缀由 subClashPath 配置
       url: >-
       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:
   structuredData:
     headings:
     headings:
       - content: >-
       - content: >-
           返回与该订阅 ID 匹配的所有已启用客户端的 base64 编码订阅链接。当请求带有 Accept:
           返回与该订阅 ID 匹配的所有已启用客户端的 base64 编码订阅链接。当请求带有 Accept:
-          text/html 头或 ?html=1 时,改为渲染一个带样式的信息页面。默认路径:/sub/:subid
+          text/html 头或 ?html=1 时,改为渲染一个带样式的信息页面。路径前缀由 subPath 配置
         id: >-
         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: >-
       - content: >-
-          以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。默认路径:/json/:subid
+          以代理配置的 JSON 数组形式返回订阅(每个已启用客户端一项)。仅在设置中启用 JSON 订阅时可用。路径前缀由 subJsonPath 配置
         id: >-
         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: >-
       - content: >-
           以兼容 Clash/Mihomo 的 YAML 配置形式返回订阅,其中包含已配置的全局 Clash 路由规则。仅在设置中启用 Clash
           以兼容 Clash/Mihomo 的 YAML 配置形式返回订阅,其中包含已配置的全局 Clash 路由规则。仅在设置中启用 Clash
-          订阅时可用。默认路径:/clash/:subid
+          订阅时可用。路径前缀由 subClashPath 配置
         id: >-
         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: []
     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 />
       <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",
       "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",
       "name": "WebSocket",
@@ -8731,7 +8731,7 @@
         "tags": [
         "tags": [
           "Clients"
           "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",
         "operationId": "get_panel_api_clients_subLinks_subId",
         "parameters": [
         "parameters": [
           {
           {
@@ -12639,7 +12639,7 @@
         "tags": [
         "tags": [
           "Subscription Server"
           "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",
         "operationId": "get_subPath_subid",
         "parameters": [
         "parameters": [
           {
           {
@@ -12698,7 +12698,7 @@
         "tags": [
         "tags": [
           "Subscription Server"
           "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",
         "operationId": "get_jsonPath_subid",
         "parameters": [
         "parameters": [
           {
           {
@@ -12748,7 +12748,7 @@
         "tags": [
         "tags": [
           "Subscription Server"
           "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",
         "operationId": "get_clashPath_subid",
         "parameters": [
         "parameters": [
           {
           {

+ 5 - 5
frontend/public/openapi.json

@@ -3512,7 +3512,7 @@
     },
     },
     {
     {
       "name": "Subscription Server",
       "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",
       "name": "WebSocket",
@@ -8731,7 +8731,7 @@
         "tags": [
         "tags": [
           "Clients"
           "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",
         "operationId": "get_panel_api_clients_subLinks_subId",
         "parameters": [
         "parameters": [
           {
           {
@@ -12639,7 +12639,7 @@
         "tags": [
         "tags": [
           "Subscription Server"
           "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",
         "operationId": "get_subPath_subid",
         "parameters": [
         "parameters": [
           {
           {
@@ -12698,7 +12698,7 @@
         "tags": [
         "tags": [
           "Subscription Server"
           "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",
         "operationId": "get_jsonPath_subid",
         "parameters": [
         "parameters": [
           {
           {
@@ -12748,7 +12748,7 @@
         "tags": [
         "tags": [
           "Subscription Server"
           "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",
         "operationId": "get_clashPath_subid",
         "parameters": [
         "parameters": [
           {
           {

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

@@ -1279,7 +1279,7 @@ export const sections: readonly Section[] = [
         method: 'GET',
         method: 'GET',
         path: '/panel/api/clients/subLinks/:subId',
         path: '/panel/api/clients/subLinks/:subId',
         summary:
         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: [
         params: [
           {
           {
             name: 'subId',
             name: 'subId',
@@ -2299,7 +2299,7 @@ export const sections: readonly Section[] = [
     id: 'subscription',
     id: 'subscription',
     title: 'Subscription Server',
     title: 'Subscription Server',
     description:
     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: [
     subHeader: [
       {
       {
         name: 'Subscription-Userinfo',
         name: 'Subscription-Userinfo',
@@ -2327,7 +2327,7 @@ export const sections: readonly Section[] = [
         method: 'GET',
         method: 'GET',
         path: '/{subPath}:subid',
         path: '/{subPath}:subid',
         summary:
         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: [
         params: [
           { name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' },
           { name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' },
           {
           {
@@ -2343,14 +2343,14 @@ export const sections: readonly Section[] = [
         method: 'GET',
         method: 'GET',
         path: '/{jsonPath}:subid',
         path: '/{jsonPath}:subid',
         summary:
         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.' }],
         params: [{ name: 'subid', in: 'path', type: 'string', desc: 'Client subscription ID.' }],
       },
       },
       {
       {
         method: 'GET',
         method: 'GET',
         path: '/{clashPath}:subid',
         path: '/{clashPath}:subid',
         summary:
         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.' }],
         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
 	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 {
 func runSeeders(isUsersEmpty bool) error {
 	empty, err := isTableEmpty("history_of_seeders")
 	empty, err := isTableEmpty("history_of_seeders")
 	if err != nil {
 	if err != nil {
@@ -2138,6 +2154,11 @@ func InitDB(dbPath string) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
+	if isUsersEmpty {
+		if err := seedRandomSubscriptionPaths(); err != nil {
+			return err
+		}
+	}
 
 
 	if err := initUser(); err != nil {
 	if err := initUser(); err != nil {
 		return err
 		return err

+ 51 - 0
internal/database/db_seed_test.go

@@ -9,6 +9,54 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 	"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) {
 func TestSeedClientsFromInboundJSON_IsIdempotentAgainstExistingClients(t *testing.T) {
 	dbDir := t.TempDir()
 	dbDir := t.TempDir()
 	t.Setenv("XUI_DB_FOLDER", dbDir)
 	t.Setenv("XUI_DB_FOLDER", dbDir)
@@ -168,6 +216,9 @@ func TestNormalizeSettingPaths_RepairsLegacyValues(t *testing.T) {
 		{Key: "subClashPath", Value: "clash/"},
 		{Key: "subClashPath", Value: "clash/"},
 		{Key: "webBasePath", Value: "/panel/"},
 		{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 {
 	for i := range seed {
 		if err := db.Create(&seed[i]).Error; err != nil {
 		if err := db.Create(&seed[i]).Error; err != nil {
 			t.Fatalf("seed setting %s: %v", seed[i].Key, err)
 			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 {
 func (s *SettingService) ResetSettings() error {
 	db := database.GetDB()
 	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) {
 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 (
 import (
 	"path/filepath"
 	"path/filepath"
+	"regexp"
 	"testing"
 	"testing"
 
 
 	"github.com/xlzd/gotp"
 	"github.com/xlzd/gotp"
@@ -10,6 +11,44 @@ import (
 	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
 	"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) {
 func setupSettingTestDB(t *testing.T) {
 	t.Helper()
 	t.Helper()
 	if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
 	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