Przeglądaj źródła

docs(api): document WireGuard and mtproto secret generation on clients/add (#6282)

* docs(api): document WireGuard and mtproto secret generation on clients/add

The POST /panel/api/clients/add summary enumerated the protocols whose
secrets the server fills in, and that list stopped being complete when
WireGuard gained per-client keys and mtproto gained a FakeTLS secret.
Read literally it says the endpoint is unusable for WireGuard without a
hand-made keypair and address, while defaultWireguardClients in fact
generates the keypair, derives the public key from a supplied private
one, and allocates a free /32.

Rather than extend an enumeration that goes stale on every new protocol,
the summary now states the rule alone and the per-protocol detail moves
into the operation description - a field Endpoint already declares and
build-openapi.mjs already maps, but that no endpoint used until now.
Swagger UI in the panel and the docs site both render it.

The attach operation gets the rule added for #5785 that nothing
documented: a client already carrying allowedIPs brings them into the
new inbound instead of being given a fresh address, and is rejected when
another client of that inbound holds it.

Closes #6276

* docs(api): correct the clients/add generation rules flagged in review

Three claims in the new description did not hold:

Shadowsocks does not keep every supplied password. fillProtocolDefaults
regenerates it when validShadowsocksClientKey rejects it, which on a
2022-blake3-* inbound means any password that does not base64-decode to
16 or 32 bytes - the call still returns success, so the caller has to
read the client back to notice. Split off from Trojan and spelled out.

The UUID is not always fresh: re-adding an email that already exists,
with the stored subId, reuses the stored id, password, auth and secret
so the identity stays in sync across its inbounds. That branch was
documented nowhere.

The mtproto secret falls back to www.cloudflare.com when the inbound
carries no fakeTlsDomain.
ilyusha 9 godzin temu
rodzic
commit
af3e6c11b6

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

@@ -28,10 +28,9 @@ _openapi:
       url: '#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to'
     - depth: 2
       title: Create a new client and attach it to one or more inbounds in a single
-        call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
-        for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
-        when omitted, so callers can send only the universal fields.
-      url: '#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields'
+        call. Body is JSON. Per-protocol secrets are generated server-side when
+        omitted, so callers can send only the universal fields.
+      url: '#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields'
     - depth: 2
       title: Update an existing client by email. Changes propagate to every attached
         inbound. Body is the JSON client payload — supply the full set of fields
@@ -278,11 +277,9 @@ _openapi:
           config IDs it is attached to.
         id: fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
       - content: Create a new client and attach it to one or more inbounds in a single
-          call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
-          password for Trojan/Shadowsocks, auth for Hysteria) are generated
-          server-side when omitted, so callers can send only the universal
-          fields.
-        id: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+          call. Body is JSON. Per-protocol secrets are generated server-side
+          when omitted, so callers can send only the universal fields.
+        id: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
       - content: Update an existing client by email. Changes propagate to every attached
           inbound. Body is the JSON client payload — supply the full set of
           fields you want to keep (the server replaces the row, it does not
@@ -477,7 +474,57 @@ _openapi:
       - content: Remove a single registered HWID device by its id, freeing one slot
           under the HWID limit.
         id: remove-a-single-registered-hwid-device-by-its-id-freeing-one-slot-under-the-hwid-limit
-    contents: []
+    contents:
+      - content: >-
+          Fields the server fills in when they are omitted — a valid value sent
+          by the caller is never overwritten. Re-adding an email that already
+          exists, with its stored `subId`, reuses the stored `id`, `password`,
+          `auth` and `secret` instead of minting new ones, so the identity stays
+          in sync across its inbounds.
+
+
+          - **VLESS / VMess** — `id`, a fresh UUID
+
+          - **Trojan** — `password`
+
+          - **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a
+          supplied password that does not base64-decode to the key length of the
+          cipher (16 or 32 bytes) is replaced by a generated key and the call
+          still succeeds, so read the client back if you did not let the server
+          pick. Legacy ciphers keep any non-empty password
+
+          - **Hysteria** — `auth`
+
+          - **mtproto** — `secret`, a FakeTLS secret derived from the fronting
+          domain of the inbound, or from `www.cloudflare.com` when it has none
+
+          - **WireGuard** — `privateKey` and `publicKey` when both are blank, or
+          `publicKey` alone when only a `privateKey` was sent, plus
+          `allowedIPs`: one free `/32` taken from the /24 the existing peers of
+          that inbound already sit in, or from `10.0.0.0/24` when it has none
+
+
+          Accepted on the same body but never generated: `preSharedKey` and
+          `keepAlive` (WireGuard), `adTag` (mtproto).
+
+
+          WireGuard is the only one of these that can fail. Allocation widens
+          the search to the containing /16 before giving up with `wireguard: no
+          free address available in <scope>`, and an `allowedIPs` supplied by
+          the caller is validated instead of allocated: `wireguard: allowedIPs
+          entry already used by another client: <address>` when a different
+          client of that same inbound already holds it. The check is per
+          inbound, so the same address on two different inbounds is accepted.
+          The same validation runs on POST /panel/api/clients/{email}/attach,
+          where a client that already carries an address brings it along.
+        heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+      - content: 'A WireGuard client brings its stored `allowedIPs` into the new inbound
+          instead of being given a fresh address, so the call fails with
+          `wireguard: allowedIPs entry already used by another client:
+          <address>` when a different client of the target inbound already holds
+          it. Free the address on that inbound first — see POST
+          /panel/api/clients/add for the full rule.'
+        heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
 ---
 
 {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

+ 57 - 10
docs/content/docs/fa/reference/api/clients.mdx

@@ -37,11 +37,10 @@ _openapi:
     - depth: 2
       title: >-
         Create a new client and attach it to one or more inbounds in a single
-        call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
-        for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
-        when omitted, so callers can send only the universal fields.
+        call. Body is JSON. Per-protocol secrets are generated server-side when
+        omitted, so callers can send only the universal fields.
       url: >-
-        #create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+        #create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
     - depth: 2
       title: >-
         Update an existing client by email. Changes propagate to every attached
@@ -352,12 +351,10 @@ _openapi:
           fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
       - content: >-
           Create a new client and attach it to one or more inbounds in a single
-          call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
-          password for Trojan/Shadowsocks, auth for Hysteria) are generated
-          server-side when omitted, so callers can send only the universal
-          fields.
+          call. Body is JSON. Per-protocol secrets are generated server-side
+          when omitted, so callers can send only the universal fields.
         id: >-
-          create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+          create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
       - content: >-
           Update an existing client by email. Changes propagate to every
           attached inbound. Body is the JSON client payload — supply the full
@@ -610,7 +607,57 @@ _openapi:
           dokodemo, tunnel) contribute nothing.
         id: >-
           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-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
-    contents: []
+    contents:
+      - content: >-
+          Fields the server fills in when they are omitted — a valid value sent
+          by the caller is never overwritten. Re-adding an email that already
+          exists, with its stored `subId`, reuses the stored `id`, `password`,
+          `auth` and `secret` instead of minting new ones, so the identity stays
+          in sync across its inbounds.
+
+
+          - **VLESS / VMess** — `id`, a fresh UUID
+
+          - **Trojan** — `password`
+
+          - **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a
+          supplied password that does not base64-decode to the key length of the
+          cipher (16 or 32 bytes) is replaced by a generated key and the call
+          still succeeds, so read the client back if you did not let the server
+          pick. Legacy ciphers keep any non-empty password
+
+          - **Hysteria** — `auth`
+
+          - **mtproto** — `secret`, a FakeTLS secret derived from the fronting
+          domain of the inbound, or from `www.cloudflare.com` when it has none
+
+          - **WireGuard** — `privateKey` and `publicKey` when both are blank, or
+          `publicKey` alone when only a `privateKey` was sent, plus
+          `allowedIPs`: one free `/32` taken from the /24 the existing peers of
+          that inbound already sit in, or from `10.0.0.0/24` when it has none
+
+
+          Accepted on the same body but never generated: `preSharedKey` and
+          `keepAlive` (WireGuard), `adTag` (mtproto).
+
+
+          WireGuard is the only one of these that can fail. Allocation widens
+          the search to the containing /16 before giving up with `wireguard: no
+          free address available in <scope>`, and an `allowedIPs` supplied by
+          the caller is validated instead of allocated: `wireguard: allowedIPs
+          entry already used by another client: <address>` when a different
+          client of that same inbound already holds it. The check is per
+          inbound, so the same address on two different inbounds is accepted.
+          The same validation runs on POST /panel/api/clients/{email}/attach,
+          where a client that already carries an address brings it along.
+        heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+      - content: 'A WireGuard client brings its stored `allowedIPs` into the new inbound
+          instead of being given a fresh address, so the call fails with
+          `wireguard: allowedIPs entry already used by another client:
+          <address>` when a different client of the target inbound already holds
+          it. Free the address on that inbound first — see POST
+          /panel/api/clients/add for the full rule.'
+        heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
 ---
 
 {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

+ 57 - 10
docs/content/docs/ru/reference/api/clients.mdx

@@ -37,11 +37,10 @@ _openapi:
     - depth: 2
       title: >-
         Create a new client and attach it to one or more inbounds in a single
-        call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
-        for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
-        when omitted, so callers can send only the universal fields.
+        call. Body is JSON. Per-protocol secrets are generated server-side when
+        omitted, so callers can send only the universal fields.
       url: >-
-        #create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+        #create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
     - depth: 2
       title: >-
         Update an existing client by email. Changes propagate to every attached
@@ -352,12 +351,10 @@ _openapi:
           fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
       - content: >-
           Create a new client and attach it to one or more inbounds in a single
-          call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
-          password for Trojan/Shadowsocks, auth for Hysteria) are generated
-          server-side when omitted, so callers can send only the universal
-          fields.
+          call. Body is JSON. Per-protocol secrets are generated server-side
+          when omitted, so callers can send only the universal fields.
         id: >-
-          create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+          create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
       - content: >-
           Update an existing client by email. Changes propagate to every
           attached inbound. Body is the JSON client payload — supply the full
@@ -610,7 +607,57 @@ _openapi:
           dokodemo, tunnel) contribute nothing.
         id: >-
           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-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
-    contents: []
+    contents:
+      - content: >-
+          Fields the server fills in when they are omitted — a valid value sent
+          by the caller is never overwritten. Re-adding an email that already
+          exists, with its stored `subId`, reuses the stored `id`, `password`,
+          `auth` and `secret` instead of minting new ones, so the identity stays
+          in sync across its inbounds.
+
+
+          - **VLESS / VMess** — `id`, a fresh UUID
+
+          - **Trojan** — `password`
+
+          - **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a
+          supplied password that does not base64-decode to the key length of the
+          cipher (16 or 32 bytes) is replaced by a generated key and the call
+          still succeeds, so read the client back if you did not let the server
+          pick. Legacy ciphers keep any non-empty password
+
+          - **Hysteria** — `auth`
+
+          - **mtproto** — `secret`, a FakeTLS secret derived from the fronting
+          domain of the inbound, or from `www.cloudflare.com` when it has none
+
+          - **WireGuard** — `privateKey` and `publicKey` when both are blank, or
+          `publicKey` alone when only a `privateKey` was sent, plus
+          `allowedIPs`: one free `/32` taken from the /24 the existing peers of
+          that inbound already sit in, or from `10.0.0.0/24` when it has none
+
+
+          Accepted on the same body but never generated: `preSharedKey` and
+          `keepAlive` (WireGuard), `adTag` (mtproto).
+
+
+          WireGuard is the only one of these that can fail. Allocation widens
+          the search to the containing /16 before giving up with `wireguard: no
+          free address available in <scope>`, and an `allowedIPs` supplied by
+          the caller is validated instead of allocated: `wireguard: allowedIPs
+          entry already used by another client: <address>` when a different
+          client of that same inbound already holds it. The check is per
+          inbound, so the same address on two different inbounds is accepted.
+          The same validation runs on POST /panel/api/clients/{email}/attach,
+          where a client that already carries an address brings it along.
+        heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+      - content: 'A WireGuard client brings its stored `allowedIPs` into the new inbound
+          instead of being given a fresh address, so the call fails with
+          `wireguard: allowedIPs entry already used by another client:
+          <address>` when a different client of the target inbound already holds
+          it. Free the address on that inbound first — see POST
+          /panel/api/clients/add for the full rule.'
+        heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
 ---
 
 {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

+ 30 - 9
docs/content/docs/zh/reference/api/clients.mdx

@@ -30,11 +30,9 @@ _openapi:
         #fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
     - depth: 2
       title: >-
-        在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥
-        (VLESS/VMess 的 UUID、Trojan/Shadowsocks 的 password、Hysteria 的 auth)在
-        省略时由服务端生成,因此调用方只需发送通用字段。
+        在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥在省略时由服务端生成,因此调用方只需发送通用字段。
       url: >-
-        #create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+        #create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
     - depth: 2
       title: >-
         按 email 更新现有客户端。变更会传播到每个挂载的入站。请求体为 JSON 客户端载荷——
@@ -290,11 +288,9 @@ _openapi:
         id: >-
           fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
       - content: >-
-          在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥
-          (VLESS/VMess 的 UUID、Trojan/Shadowsocks 的 password、Hysteria 的 auth)在
-          省略时由服务端生成,因此调用方只需发送通用字段。
+          在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥在省略时由服务端生成,因此调用方只需发送通用字段。
         id: >-
-          create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+          create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
       - content: >-
           按 email 更新现有客户端。变更会传播到每个挂载的入站。请求体为 JSON 客户端载荷——
           请提供你希望保留的完整字段集(服务端会替换整条记录,而非局部更新)。
@@ -493,7 +489,32 @@ _openapi:
           (socks、http、mixed、wireguard、dokodemo、tunnel)不产生任何内容。
         id: >-
           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-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
-    contents: []
+    contents:
+      - content: >-
+          服务端在字段被省略时自动填充;调用方提供的有效值不会被覆盖。若以已存在的 email 重新添加,且其已存储的 `subId` 一致,则沿用已存储的 `id`、`password`、`auth` 和 `secret`,而不是重新生成,以保证同一身份在其各个入站之间保持一致。
+
+
+          - **VLESS / VMess** —— `id`,新生成的 UUID
+
+          - **Trojan** —— `password`
+
+          - **Shadowsocks** —— `password`。在 `2022-blake3-*` 入站上,若调用方提供的 password 经 base64 解码后的长度不等于该加密方式所需的密钥长度(16 或 32 字节),它会被替换为服务端生成的密钥,且调用仍然返回成功;因此若不打算交由服务端生成,请回读该客户端确认。传统加密方式则保留任何非空 password
+
+          - **Hysteria** —— `auth`
+
+          - **mtproto** —— `secret`,由该入站的伪装域名派生的 FakeTLS 密钥;该入站未设置伪装域名时,则取自 `www.cloudflare.com`
+
+          - **WireGuard** —— 两个密钥都为空时生成 `privateKey` 与 `publicKey`;只提供了 `privateKey` 时仅推导 `publicKey`。此外还会分配 `allowedIPs`:从该入站现有对端所在的 /24 中取一个空闲的 `/32`,若该入站尚无对端,则取自 `10.0.0.0/24`
+
+
+          同一请求体也接受、但服务端不会自动生成的字段:`preSharedKey` 与 `keepAlive`(WireGuard)、`adTag`(mtproto)。
+
+
+          其中只有 WireGuard 这一步可能失败。分配地址时会先把搜索范围扩大到所属的 /16,之后才以 `wireguard: no free address available in <scope>` 放弃;而调用方自行提供的 `allowedIPs` 只做校验、不做分配:当同一入站上的另一个客户端已占用该地址时,返回 `wireguard: allowedIPs entry already used by another client: <address>`。该校验按入站进行,因此同一地址出现在两个不同入站上是允许的。POST /panel/api/clients/{email}/attach 也执行同样的校验——已带有地址的客户端会把该地址带入新的入站。
+        heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
+      - content: >-
+          WireGuard 客户端会把已存储的 `allowedIPs` 带入新入站,而不是获得新分配的地址;因此当目标入站上的另一个客户端已占用该地址时,调用会以 `wireguard: allowedIPs entry already used by another client: <address>` 失败。请先在该入站上释放该地址——完整规则见 POST /panel/api/clients/add。
+        heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
 ---
 
 {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}

+ 3 - 1
docs/public/openapi.json

@@ -4976,8 +4976,9 @@
         "tags": [
           "Clients"
         ],
-        "summary": "Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password for Trojan/Shadowsocks, auth for Hysteria) are generated server-side when omitted, so callers can send only the universal fields.",
+        "summary": "Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets are generated server-side when omitted, so callers can send only the universal fields.",
         "operationId": "post_panel_api_clients_add",
+        "description": "Fields the server fills in when they are omitted — a valid value sent by the caller is never overwritten. Re-adding an email that already exists, with its stored `subId`, reuses the stored `id`, `password`, `auth` and `secret` instead of minting new ones, so the identity stays in sync across its inbounds.\n\n- **VLESS / VMess** — `id`, a fresh UUID\n- **Trojan** — `password`\n- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a supplied password that does not base64-decode to the key length of the cipher (16 or 32 bytes) is replaced by a generated key and the call still succeeds, so read the client back if you did not let the server pick. Legacy ciphers keep any non-empty password\n- **Hysteria** — `auth`\n- **mtproto** — `secret`, a FakeTLS secret derived from the fronting domain of the inbound, or from `www.cloudflare.com` when it has none\n- **WireGuard** — `privateKey` and `publicKey` when both are blank, or `publicKey` alone when only a `privateKey` was sent, plus `allowedIPs`: one free `/32` taken from the /24 the existing peers of that inbound already sit in, or from `10.0.0.0/24` when it has none\n\nAccepted on the same body but never generated: `preSharedKey` and `keepAlive` (WireGuard), `adTag` (mtproto).\n\nWireGuard is the only one of these that can fail. Allocation widens the search to the containing /16 before giving up with `wireguard: no free address available in <scope>`, and an `allowedIPs` supplied by the caller is validated instead of allocated: `wireguard: allowedIPs entry already used by another client: <address>` when a different client of that same inbound already holds it. The check is per inbound, so the same address on two different inbounds is accepted. The same validation runs on POST /panel/api/clients/{email}/attach, where a client that already carries an address brings it along.",
         "requestBody": {
           "required": true,
           "content": {
@@ -5152,6 +5153,7 @@
         ],
         "summary": "Attach an existing client to one or more additional inbounds. Body is JSON.",
         "operationId": "post_panel_api_clients_email_attach",
+        "description": "A WireGuard client brings its stored `allowedIPs` into the new inbound instead of being given a fresh address, so the call fails with `wireguard: allowedIPs entry already used by another client: <address>` when a different client of the target inbound already holds it. Free the address on that inbound first — see POST /panel/api/clients/add for the full rule.",
         "parameters": [
           {
             "name": "email",

+ 3 - 1
frontend/public/openapi.json

@@ -6245,8 +6245,9 @@
         "tags": [
           "Clients"
         ],
-        "summary": "Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password for Trojan/Shadowsocks, auth for Hysteria) are generated server-side when omitted, so callers can send only the universal fields.",
+        "summary": "Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets are generated server-side when omitted, so callers can send only the universal fields.",
         "operationId": "post_panel_api_clients_add",
+        "description": "Fields the server fills in when they are omitted — a valid value sent by the caller is never overwritten. Re-adding an email that already exists, with its stored `subId`, reuses the stored `id`, `password`, `auth` and `secret` instead of minting new ones, so the identity stays in sync across its inbounds.\n\n- **VLESS / VMess** — `id`, a fresh UUID\n- **Trojan** — `password`\n- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a supplied password that does not base64-decode to the key length of the cipher (16 or 32 bytes) is replaced by a generated key and the call still succeeds, so read the client back if you did not let the server pick. Legacy ciphers keep any non-empty password\n- **Hysteria** — `auth`\n- **mtproto** — `secret`, a FakeTLS secret derived from the fronting domain of the inbound, or from `www.cloudflare.com` when it has none\n- **WireGuard** — `privateKey` and `publicKey` when both are blank, or `publicKey` alone when only a `privateKey` was sent, plus `allowedIPs`: one free `/32` taken from the /24 the existing peers of that inbound already sit in, or from `10.0.0.0/24` when it has none\n\nAccepted on the same body but never generated: `preSharedKey` and `keepAlive` (WireGuard), `adTag` (mtproto).\n\nWireGuard is the only one of these that can fail. Allocation widens the search to the containing /16 before giving up with `wireguard: no free address available in <scope>`, and an `allowedIPs` supplied by the caller is validated instead of allocated: `wireguard: allowedIPs entry already used by another client: <address>` when a different client of that same inbound already holds it. The check is per inbound, so the same address on two different inbounds is accepted. The same validation runs on POST /panel/api/clients/{email}/attach, where a client that already carries an address brings it along.",
         "requestBody": {
           "required": true,
           "content": {
@@ -6423,6 +6424,7 @@
         ],
         "summary": "Attach an existing client to one or more additional inbounds. Body is JSON.",
         "operationId": "post_panel_api_clients_email_attach",
+        "description": "A WireGuard client brings its stored `allowedIPs` into the new inbound instead of being given a fresh address, so the call fails with `wireguard: allowedIPs entry already used by another client: <address>` when a different client of the target inbound already holds it. Free the address on that inbound first — see POST /panel/api/clients/add for the full rule.",
         "parameters": [
           {
             "name": "email",

+ 6 - 2
frontend/src/pages/api-docs/endpoints.ts

@@ -844,13 +844,15 @@ export const sections: readonly Section[] = [
         method: 'POST',
         path: '/panel/api/clients/add',
         summary:
-          'Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password for Trojan/Shadowsocks, auth for Hysteria) are generated server-side when omitted, so callers can send only the universal fields.',
+          'Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets are generated server-side when omitted, so callers can send only the universal fields.',
+        description:
+          'Fields the server fills in when they are omitted — a valid value sent by the caller is never overwritten. Re-adding an email that already exists, with its stored `subId`, reuses the stored `id`, `password`, `auth` and `secret` instead of minting new ones, so the identity stays in sync across its inbounds.\n\n- **VLESS / VMess** — `id`, a fresh UUID\n- **Trojan** — `password`\n- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a supplied password that does not base64-decode to the key length of the cipher (16 or 32 bytes) is replaced by a generated key and the call still succeeds, so read the client back if you did not let the server pick. Legacy ciphers keep any non-empty password\n- **Hysteria** — `auth`\n- **mtproto** — `secret`, a FakeTLS secret derived from the fronting domain of the inbound, or from `www.cloudflare.com` when it has none\n- **WireGuard** — `privateKey` and `publicKey` when both are blank, or `publicKey` alone when only a `privateKey` was sent, plus `allowedIPs`: one free `/32` taken from the /24 the existing peers of that inbound already sit in, or from `10.0.0.0/24` when it has none\n\nAccepted on the same body but never generated: `preSharedKey` and `keepAlive` (WireGuard), `adTag` (mtproto).\n\nWireGuard is the only one of these that can fail. Allocation widens the search to the containing /16 before giving up with `wireguard: no free address available in <scope>`, and an `allowedIPs` supplied by the caller is validated instead of allocated: `wireguard: allowedIPs entry already used by another client: <address>` when a different client of that same inbound already holds it. The check is per inbound, so the same address on two different inbounds is accepted. The same validation runs on POST /panel/api/clients/{email}/attach, where a client that already carries an address brings it along.',
         params: [
           {
             name: 'client',
             in: 'body (json)',
             type: 'object',
-            desc: 'Client fields: email, subId, id (uuid), password, auth, flow, totalGB, expiryTime, limitIp, limitHwid, tgId (numeric Telegram user ID, 0 = none), comment, enable.',
+            desc: 'Client fields: email, subId, id (uuid), password, auth, flow, totalGB, expiryTime, limitIp, limitHwid, tgId (numeric Telegram user ID, 0 = none), comment, enable. Protocol-specific: secret and adTag (mtproto), privateKey, publicKey, preSharedKey, allowedIPs and keepAlive (WireGuard).',
           },
           {
             name: 'inboundIds',
@@ -898,6 +900,8 @@ export const sections: readonly Section[] = [
         method: 'POST',
         path: '/panel/api/clients/:email/attach',
         summary: 'Attach an existing client to one or more additional inbounds. Body is JSON.',
+        description:
+          'A WireGuard client brings its stored `allowedIPs` into the new inbound instead of being given a fresh address, so the call fails with `wireguard: allowedIPs entry already used by another client: <address>` when a different client of the target inbound already holds it. Free the address on that inbound first — see POST /panel/api/clients/add for the full rule.',
         params: [
           { name: 'email', in: 'path', type: 'string', desc: 'Client email (unique identifier).' },
           {