From ad18e16ac69bf4596defc475db1dcd188e826d83 Mon Sep 17 00:00:00 2001 From: aaaamoooo Date: Tue, 20 Oct 2026 21:21:13 +0000 Subject: [PATCH] feat: customer-level credentials shared across all nodes (item 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One credential set per customer (UUID + 32-byte root relay key + shortId) lives on the customer record; node bindings no longer store credentials. SS2022 user keys derive deterministically from the root key by method length (16/24/32-byte prefix), so a single password serves every protocol, matching 3x-ui's per-client credential model. Reality public keys stay a node property and are shown per binding in the credentials pane. - store: load-time convergence — first binding's set wins; a converged password that is not a 32-byte root key is regenerated (uuid/shortId preserved); bindings are stripped; idempotent, covered by tests - deploy: the orchestrator stamps the customer's set onto a build copy; subscriptions resolve the same set; the derivation applies in both the inbound builder and the URI builder (verified live: Alice's N1 link is byte-identical to the pre-change seed expectation, her N2 user key is exactly the root key's 16-byte prefix) - API: GET /api/customers/:id/credentials returns {credentials, bindings}; regenerate is now POST /api/customers/:id/credentials/regenerate and rotates the whole set, rebuilding every deployed node of the customer - UI: credentials pane shows the single shared set plus per-binding node/protocol/Reality-public-key rows - golden subscriptions fixture regenerated under one shared set (RF and SS entries byte-identical to before; five entries audited by hand) - golden baseline re-captured for the new shapes; difftest normalizer now treats a base64 root key's prefix-truncation encodings and the Surge '='-mangled variants as the same secret --- LINK-MIGRATION.md | 72 +++++++ NEXT-ROUND-NOTES.md | 18 ++ panel-go/credentials_test.go | 124 +++++++++++ panel-go/difftest/lib.mjs | 27 ++- panel-go/golden/golden.json | 227 +++++++++++--------- panel-go/handlers.go | 135 ++++++------ panel-go/orchestrator.go | 55 +++-- panel-go/protocols.go | 60 ++++-- panel-go/protocols_test.go | 6 +- panel-go/store.go | 61 ++++++ panel-go/subscriptions.go | 10 +- panel-go/subscriptions_test.go | 25 ++- panel-go/testdata/golden_subscriptions.json | 64 +++--- panel-go/types.go | 30 ++- panel-go/uireskin_test.go | 7 +- public/app.js | 24 ++- 16 files changed, 673 insertions(+), 272 deletions(-) create mode 100644 LINK-MIGRATION.md create mode 100644 panel-go/credentials_test.go diff --git a/LINK-MIGRATION.md b/LINK-MIGRATION.md new file mode 100644 index 0000000..4864d44 --- /dev/null +++ b/LINK-MIGRATION.md @@ -0,0 +1,72 @@ +# NexusGate 分发链接迁移报告(GitHub → 自建 Gitea) + +- 日期:2026-10-20 +- 提交:`f4ef3d9` chore: migrate self-distribution links from GitHub to self-hosted Gitea(8 文件,本地已 commit,**未 push**,等云电脑侧续推) +- 新基址:`https://git.881218.xyz/rogue/NexusGate` + +## 映射规则(已按此执行) + +| 类型 | 旧(GitHub) | 新(Gitea) | +|---|---|---| +| raw | `raw.githubusercontent.com/aaaamoooo/NexusGate//` | `git.881218.xyz/rogue/NexusGate/raw/branch//` | +| release | `github.com/aaaamoooo/NexusGate/releases/download//` | `git.881218.xyz/rogue/NexusGate/releases/download//` | +| archive | `github.com//archive/refs/heads/.tar.gz` | `git.881218.xyz/rogue/NexusGate/archive/.tar.gz` | +| clone | `github.com/aaaamoooo/NexusGate.git` | `git.881218.xyz/rogue/NexusGate.git` | + +## 逐文件改动 + +- `public/app.js`(2 处):面板生成的节点注册命令、旧版卸载兼容命令的 raw 链接 → Gitea raw。 +- `panel-go/embed_public/app.js`:同上同步改毕,两份 app.js 全文件逐字一致。注意该目录是 panel-go 的构建产物(`.gitignore` 排除,`build.sh` 每次从 `../public` 重新拷贝),不入仓;panel-go 已编译的三个二进制(同为 gitignore 产物)内嵌的仍是旧链接,**需重跑 panel-go/build.sh 重建后才生效**。 +- `scripts/agent-install.sh` / `scripts/agent-update.sh`:新增 `GIT_BASE="https://git.881218.xyz/${REPO}"`,raw 与 release 链接改由 GIT_BASE 拼装;`REPO` 默认 `aaaamoooo/NexusGate` → `rogue/NexusGate`;`NG_REPO`/`NG_BRANCH`/`--repo`/`--branch`/`NG_AGENT_RELEASE` 语义与默认值不变;raw 的 `?t=` 缓存破坏参数保留。相关注释 "GitHub Release" → "Gitea Release"。 +- `scripts/install.sh` / `scripts/nexusgate.sh`:同上加 GIT_BASE;归档链接改 Gitea archive 格式(两脚本的解包逻辑对顶层目录的处理与 Gitea 包结构兼容:install.sh 取唯一顶层目录、nexusgate.sh 用 --strip-components=1)。 +- `scripts/agent-singbox.sh`:release 基址改为 `https://git.881218.xyz/${NG_REPO:-rogue/NexusGate}/releases/download/${release_tag}`(调用方 agent-install.sh 本就透传 NG_REPO);stats-query.go 的 raw 拉取改 Gitea raw 格式;提示文案 GitHub → Gitea。 +- `README.md`:clone 地址、一键注册命令 raw 链接、"GitHub Release" 文案。 +- `agent-go/INSTALL-GO.md`:release 下载地址与文案。 + +## ⚠️ 判断性改动(请复核) + +以下三处脚本的 `REPO` 默认值原本指向**上游原作者** `a2899882/NexusGate`,本次一并改为 `rogue/NexusGate`: + +- `scripts/install.sh`、`scripts/nexusgate.sh` 的源码归档下载 +- `scripts/agent-singbox.sh` 中 stats-query.go 的拉取源 + +理由:这三处下载的是**本 fork 的面板/Agent 代码本身**(上游原版没有本 fork 的二开改动,装错即装成原版),且 a2899882 在自建 Gitea 上不存在、保留只会 404。调用方均可继续用 `NG_REPO` 覆盖。如认为应保留上游默认,回滚这三行即可。 + +## 未改动(残留清单及理由) + +- `agent/stats-query.go` 的 import、`scripts/agent-singbox.sh` 的 `go install`/`go get github.com/sagernet/sing-box@...`、`agent-go/PARITY.md` 中的提及:均为**上游 sing-box 依赖包名**,不是分发链接。 +- `scripts/agent-singbox.sh` 中 Go 工具链下载 `go.dev/dl/...`:上游官方地址、非 GitHub。 +- `.github/workflows/ci.yml`:GitHub Actions 配置(actions/checkout 等),与分发无关;Gitea 侧默认不执行,保持原样。 +- 全仓已复查:无 ghproxy / jsdelivr 等镜像域名、无其他写死的旧分发域名。 + +## 自检结果 + +- grep 复查:文本文件中已无指向 `aaaamoooo` / `a2899882` / `imamoxilin` 或以 `${REPO}` 拼 GitHub 主机的自有分发链接(残留仅上表上游依赖)。 +- `bash -n` 五个改过的脚本全部通过。 +- `public/app.js` 与 `panel-go/embed_public/app.js` diff 为空(逐字一致)。 + +## ★ Gitea 上尚不存在、需手工建的 Release 清单 + +脚本已指向以下地址,但在把文件传上去之前它们是 404。raw 与源码归档由 Gitea 按仓自动提供,无需手工处理;**只有这两组 release 要建**: + +### 1. 标签 `agent-v0.6.8`(agent-install.sh / agent-update.sh 的 `AGENT_RELEASE` 默认值) + +| 附件名(必须逐字一致) | 本地现存文件 | +|---|---| +| `nexusgate-agent-linux-amd64` | `~/workspace/backups/2026-10-20/binaries/nexusgate-agent-linux-amd64` | +| `nexusgate-agent-linux-arm64` | `~/workspace/backups/2026-10-20/binaries/nexusgate-agent-linux-arm64` | + +(同名同大小副本也在 `~/workspace/NexusGate/agent-go/` 下,任选一处上传,文件名不用改。) + +### 2. 标签 `sing-box-v1.13.21`(agent-singbox.sh 的 `release_tag` 默认值,可由 `NG_SINGBOX_RELEASE` 覆盖) + +| 附件名(必须逐字一致) | 本地现存文件(上传时需改名去掉 `-v1.13.21` 后缀) | +|---|---| +| `nexusgate-sing-box-linux-amd64` | `~/workspace/nexusgate-release/bin/nexusgate-sing-box-linux-amd64-v1.13.21` | +| `nexusgate-sing-box-linux-arm64` | `~/workspace/nexusgate-release/bin/nexusgate-sing-box-linux-arm64-v1.13.21` | +| `nexusgate-sing-box-stats-linux-amd64` | `~/workspace/nexusgate-release/bin/nexusgate-sing-box-stats-linux-amd64-v1.13.21` | +| `nexusgate-sing-box-stats-linux-arm64` | `~/workspace/nexusgate-release/bin/nexusgate-sing-box-stats-linux-arm64-v1.13.21` | + +(`~/workspace/backups/2026-10-20/binaries/` 下有同名镜像副本。) + +注:sing-box 预编译下载失败时脚本会自动回退现场源码编译,所以这组 release 缺失不会让装机彻底失败,只是慢;agent 那组缺失则 Go 版 agent 无法新装(Node 回退路径走 raw,不受影响)。 diff --git a/NEXT-ROUND-NOTES.md b/NEXT-ROUND-NOTES.md index 5c7c1b8..2540dab 100644 --- a/NEXT-ROUND-NOTES.md +++ b/NEXT-ROUND-NOTES.md @@ -72,3 +72,21 @@ amo 各页主内容区两侧留白偏大;把左右间距(padding/最大宽 ## 10. 删除顶栏全局搜索框(2026-10-21 04:39,用户附顶栏截图) 顶栏"搜索服务器、客户、节点…"输入框是写死 disabled 的摆设、无任何功能;且每页都有自己的可用搜索框,此框多余——删除(顶栏共用一处,删一处全站生效)。 + +## 11. 菜单与命名调整 + 页面合并(2026-10-21 04:47,用户附节点页全图) + +- 左侧菜单「客户与订阅」→ 改名「用户订阅」(页面标题/面包屑同步) +- 左侧菜单「服务器」→ 改名「节点管理」 +- 左侧菜单「转发与节点」→ 改名「入站&出站」(页面标题/面包屑同步) +- 该页「+ 新建节点」按钮 → 改为「添加入站」;页内计数文案「9 个节点」→「9 个入站」(同页其他"节点"计数文案同步对齐) +- 左侧菜单「设置与运维」→ 改名「设置」 +- 「安装与命令」页面的全部内容合并进「设置」页面,合并后删除「安装与命令」页面(菜单项与路由一并摘除) + +## 12. 删除系统状态页「最近活动」模块(2026-10-21 04:56,用户附截图) + +首页(系统状态页)底部的「最近活动」面板整个删除(该面板是改版时为保旧功能补回的,用户现明确不要)。只删展示模块,后端活动/审计记录本身保留不动。 + +## 13. 设置页「任务队列」表格排紧凑(2026-10-21 04:58,用户附截图) + +现状毛病:列宽分配失衡——任务 ID 被挤到换行两行、状态"已完成"竖排三行、时间两行,而错误列大片空白。 +要求:重配列宽与换行——ID/状态/时间等短内容不换行、单行呈现;空白列不占大宽度;整表紧凑。合并进设置页后保持此排版。 diff --git a/panel-go/credentials_test.go b/panel-go/credentials_test.go new file mode 100644 index 0000000..d18c2a7 --- /dev/null +++ b/panel-go/credentials_test.go @@ -0,0 +1,124 @@ +package main + +// Tests for the customer-level credential model (round-2 item 2): +// store-load convergence from legacy per-binding credentials, the +// deterministic password rule, and the SS2022 user-key derivation. + +import ( + "encoding/base64" + "testing" +) + +func TestMigrateCustomerCredentialsConvergesFirstBinding(t *testing.T) { + d := &Data{ + Customers: []*Customer{{ID: "cus_1", Name: "Legacy"}}, + NodeClients: []*NodeClient{ + {ID: "ncl_1", NodeID: "nod_a", CustomerID: "cus_1", Credentials: &Credentials{ + ClientID: "11111111-2222-3333-4444-000000000001", + // A 32-byte root key: must be kept verbatim. + RelayPassword: base64.StdEncoding.EncodeToString(make([]byte, 32)), + ShortID: "abcd0001ef01", + }}, + {ID: "ncl_2", NodeID: "nod_b", CustomerID: "cus_1", Credentials: &Credentials{ + ClientID: "22222222-2222-3333-4444-000000000002", RelayPassword: "other", ShortID: "ffff0002ef01", + }}, + }, + } + if !MigrateCustomerCredentials(d) { + t.Fatalf("migration reported no change") + } + creds := d.Customers[0].Credentials + if creds == nil || creds.ClientID != "11111111-2222-3333-4444-000000000001" || creds.ShortID != "abcd0001ef01" { + t.Fatalf("converged set is not the first binding's: %+v", creds) + } + if creds.RelayPassword != base64.StdEncoding.EncodeToString(make([]byte, 32)) { + t.Fatalf("32-byte root key must be kept, got %q", creds.RelayPassword) + } + for _, nc := range d.NodeClients { + if nc.Credentials != nil { + t.Fatalf("binding %s still carries credentials", nc.ID) + } + } + if MigrateCustomerCredentials(d) { + t.Fatalf("migration must be idempotent") + } +} + +func TestMigrateCustomerCredentialsRegeneratesShortPasswordOnly(t *testing.T) { + d := &Data{ + Customers: []*Customer{{ID: "cus_1"}}, + NodeClients: []*NodeClient{ + {ID: "ncl_1", NodeID: "nod_a", CustomerID: "cus_1", Credentials: &Credentials{ + ClientID: "11111111-2222-3333-4444-000000000001", + // 16-byte key (an old SS2022-aes128 binding set): too short + // to derive a 32-byte user key from. + RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", + ShortID: "abcd0001ef01", + }}, + }, + } + MigrateCustomerCredentials(d) + creds := d.Customers[0].Credentials + if creds.ClientID != "11111111-2222-3333-4444-000000000001" || creds.ShortID != "abcd0001ef01" { + t.Fatalf("clientId/shortId must be preserved: %+v", creds) + } + raw, err := base64.StdEncoding.DecodeString(creds.RelayPassword) + if err != nil || len(raw) != 32 { + t.Fatalf("password must be regenerated as a 32-byte root key, got %q", creds.RelayPassword) + } + if creds.RelayPassword == "FRYXGBkaGxwdHh8gISIjJA==" { + t.Fatalf("short password must not survive convergence") + } +} + +func TestMigrateCustomerCredentialsFreshForBindingless(t *testing.T) { + d := &Data{Customers: []*Customer{{ID: "cus_new"}}} + if !MigrateCustomerCredentials(d) { + t.Fatalf("expected a change") + } + creds := d.Customers[0].Credentials + if creds == nil || creds.ClientID == "" || creds.ShortID == "" { + t.Fatalf("fresh set incomplete: %+v", creds) + } + if raw, err := base64.StdEncoding.DecodeString(creds.RelayPassword); err != nil || len(raw) != 32 { + t.Fatalf("fresh set password is not a 32-byte root key") + } +} + +func TestEffectiveRelayPassword(t *testing.T) { + root := make([]byte, 32) + for i := range root { + root[i] = byte(i) + } + rootB64 := base64.StdEncoding.EncodeToString(root) + // aes128 (16-byte method): deterministic prefix truncation. + if got := EffectiveRelayPassword("shadowsocks-2022-aes128", rootB64); got != base64.StdEncoding.EncodeToString(root[:16]) { + t.Fatalf("aes128 derivation wrong: %q", got) + } + // aes256: the root key itself. + if got := EffectiveRelayPassword("shadowsocks-2022-aes256", rootB64); got != rootB64 { + t.Fatalf("aes256 derivation wrong: %q", got) + } + // Legacy methods and other protocols: verbatim. + if got := EffectiveRelayPassword("shadowsocks-aes256-gcm", rootB64); got != rootB64 { + t.Fatalf("legacy SS must be verbatim") + } + if got := EffectiveRelayPassword("hysteria2", "plain-password"); got != "plain-password" { + t.Fatalf("non-SS must be verbatim") + } + // An exact-length legacy key passes through unchanged (identity). + k16 := "FRYXGBkaGxwdHh8gISIjJA==" + if got := EffectiveRelayPassword("shadowsocks-2022-aes128", k16); got != k16 { + t.Fatalf("16-byte key on aes128 must be identity, got %q", got) + } +} + +func TestNewCustomerCredentialsShape(t *testing.T) { + c := NewCustomerCredentials() + if c.ClientID == "" || c.ShortID == "" { + t.Fatalf("incomplete set: %+v", c) + } + if raw, err := base64.StdEncoding.DecodeString(c.RelayPassword); err != nil || len(raw) != 32 { + t.Fatalf("relay password must be a base64 32-byte root key") + } +} diff --git a/panel-go/difftest/lib.mjs b/panel-go/difftest/lib.mjs index b5ad7ba..85c4b7a 100644 --- a/panel-go/difftest/lib.mjs +++ b/panel-go/difftest/lib.mjs @@ -392,11 +392,30 @@ export class Normalizer { if (this.panelOrigin) out = out.split(this.panelOrigin).join(''); // Learned secrets first (longest raw first so a full token wins over its // own prefixes), including the 16-char prefix shown on the info page. - const secrets = [...this.secrets.entries()].sort((a, b) => b[0].length - a[0].length); - for (const [raw, placeholder] of secrets) { - if (raw.length >= 8) out = out.split(raw).join(placeholder); - if (raw.length > 16) out = out.split(raw.slice(0, 16)).join(placeholder); + // A base64 secret's prefix truncations (the SS2022 user keys derived + // from a customer's 32-byte root key) are the same logical secret, so + // their encodings are replaced with the same placeholder. + const replacements = []; + for (const [raw, placeholder] of this.secrets) { + if (raw.length >= 8) replacements.push([raw, placeholder]); + if (raw.length > 16) replacements.push([raw.slice(0, 16), placeholder]); + if (/^[A-Za-z0-9+/]+={0,2}$/.test(raw) && raw.length % 4 === 0) { + const bytes = Buffer.from(raw, 'base64'); + if (bytes.toString('base64') === raw) { + for (const n of [16, 24]) { + if (n < bytes.length) replacements.push([bytes.subarray(0, n).toString('base64'), placeholder]); + } + } + } } + // The Surge formatter mangles '=' to '_' inside passwords, so the + // mangled variant of every replacement is the same secret too. + for (const [raw, placeholder] of [...replacements]) { + const mangled = raw.replace(/=/g, '_'); + if (mangled !== raw) replacements.push([mangled, placeholder]); + } + replacements.sort((a, b) => b[0].length - a[0].length); + for (const [raw, placeholder] of replacements) out = out.split(raw).join(placeholder); for (const [raw, placeholder] of this.hashes) out = out.split(raw).join(placeholder); out = out.replace(PREFIXED_ID_RE, (whole, prefix) => this.#idPlaceholder(prefix, whole)); for (const [fragment, placeholder] of this.fragments) out = out.split(fragment).join(placeholder); diff --git a/panel-go/golden/golden.json b/panel-go/golden/golden.json index 2863026..1210bf3 100644 --- a/panel-go/golden/golden.json +++ b/panel-go/golden/golden.json @@ -1,6 +1,6 @@ { "meta": { - "capturedAt": "2026-10-20T21:02:25.354Z", + "capturedAt": "2026-10-20T21:20:39.194Z", "note": "Go panel golden baseline; see README.md in this directory." }, "sections": { @@ -49,6 +49,11 @@ "customers": [ { "createdAt": "", + "credentials": { + "clientId": "#1", + "relayPassword": "#1", + "shortId": "#2" + }, "deviceLimit": 0, "expiresAt": null, "group": "", @@ -83,6 +88,11 @@ }, { "createdAt": "", + "credentials": { + "clientId": "#2", + "relayPassword": "#3", + "shortId": "#4" + }, "deviceLimit": 2, "expiresAt": null, "group": "", @@ -170,7 +180,7 @@ "body": { "nodeClients": [ { - "clientUri": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=45584502b5567121&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", + "clientUri": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "createdAt": "", "customerId": "#1", "id": "#1", @@ -179,7 +189,7 @@ "updatedAt": "" }, { - "clientUri": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=0adf5e3d9a002211&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", + "clientUri": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "createdAt": "", "customerId": "#2", "id": "#2", @@ -248,7 +258,7 @@ "epoch": "diff-epoch-1", "uplink": 4000 }, - "wsPath": "#1" + "wsPath": "#5" }, { "artifacts": {}, @@ -286,7 +296,7 @@ "topology": "direct", "updatedAt": "", "usageCounters": null, - "wsPath": "#2" + "wsPath": "#6" } ] }, @@ -555,51 +565,55 @@ "credentials": { "alice": { "body": { - "customerId": "#1", - "items": [ + "bindings": [ { - "clientId": "#1", "nodeClientId": "#1", "nodeId": "#2", "nodeName": "N1", - "password": "#3", "protocol": "vless-reality-vision", - "shortId": "#4", + "realityPublicKey": "#1", "status": "active", "username": "" }, { - "clientId": "#3", "nodeClientId": "#3", "nodeId": "#1", "nodeName": "N2", - "password": "#5", "protocol": "shadowsocks-2022-aes128", - "shortId": "#6", + "realityPublicKey": null, "status": "active", "username": "" } ], + "credentials": { + "clientId": "#1", + "password": "#1", + "shortId": "#2" + }, + "customerId": "#1", "subscriptionToken": "#1" }, "status": 200 }, "bob": { "body": { - "customerId": "#2", - "items": [ + "bindings": [ { - "clientId": "#2", "nodeClientId": "#2", "nodeId": "#2", "nodeName": "N1", - "password": "#7", "protocol": "vless-reality-vision", - "shortId": "#8", + "realityPublicKey": "#1", "status": "active", "username": "" } ], + "credentials": { + "clientId": "#2", + "password": "#3", + "shortId": "#4" + }, + "customerId": "#2", "subscriptionToken": "#2" }, "status": 200 @@ -608,49 +622,49 @@ "subscriptionsSeed": { "alice": { "auto-clash": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#4\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: #3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:#5\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\n - \"N2\"\nrules:\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#2\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: #3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:#1\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\n - \"N2\"\nrules:\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" }, "auto-generic": { - "body": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://#1@entry.example.com:#3#N2\n", + "body": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://#1@entry.example.com:#3#N2\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" }, "base64": { - "body": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://#1@entry.example.com:#3#N2\n", + "body": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://#1@entry.example.com:#3#N2\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" }, "clash": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#4\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: #3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:#5\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\n - \"N2\"\nrules:\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#2\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: #3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:#1\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\n - \"N2\"\nrules:\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" }, "clash-smart": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#4\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: #3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:#5\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"N1\"\n - \"N2\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#2\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: #3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:#1\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"N1\"\n - \"N2\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - \"N2\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" }, "raw": { - "body": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://#1@entry.example.com:#3#N2\n", + "body": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://#1@entry.example.com:#3#N2\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" }, "singbox": { - "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\",\n \"node-2\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": #2,\n \"uuid\": \"#1\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"#1\",\n \"short_id\": \"#4\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": #3,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"xOcB7XGgo6VpaEHZfNXrlQ==:#5\"\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", + "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\",\n \"node-2\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": #2,\n \"uuid\": \"#1\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"#1\",\n \"short_id\": \"#2\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": #3,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"xOcB7XGgo6VpaEHZfNXrlQ==:#1\"\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", "contentType": "application/json; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" }, "surge": { - "body": "[General]\nloglevel = notify\n[Proxy]\nN2 = ss, entry.example.com, #3, encrypt-method=2022-blake3-aes-128-gcm, password=xOcB7XGgo6VpaEHZfNXrlQ__:#5VgHuXw__\n[Proxy Group]\n节点选择 = select, N2, DIRECT\n自动选择 = url-test, N2, url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n", + "body": "[General]\nloglevel = notify\n[Proxy]\nN2 = ss, entry.example.com, #3, encrypt-method=2022-blake3-aes-128-gcm, password=xOcB7XGgo6VpaEHZfNXrlQ__:#1\n[Proxy Group]\n节点选择 = select, N2, DIRECT\n自动选择 = url-test, N2, url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=1000; download=2000; total=0; expire=0" @@ -658,7 +672,7 @@ }, "bob": { "auto-clash": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#8\"\n flow: \"xtls-rprx-vision\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\nrules:\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#4\"\n flow: \"xtls-rprx-vision\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\nrules:\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=1073741824; expire=0" @@ -670,31 +684,31 @@ "userinfo": "" }, "base64": { - "body": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n", + "body": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=1073741824; expire=0" }, "clash": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#8\"\n flow: \"xtls-rprx-vision\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\nrules:\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#4\"\n flow: \"xtls-rprx-vision\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\nrules:\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=1073741824; expire=0" }, "clash-smart": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#8\"\n flow: \"xtls-rprx-vision\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"N1\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"N1\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: #2\n type: \"vless\"\n uuid: \"#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#1\"\n short-id: \"#4\"\n flow: \"xtls-rprx-vision\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"N1\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"N1\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N1\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=1073741824; expire=0" }, "raw": { - "body": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n", + "body": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=1073741824; expire=0" }, "singbox": { - "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": #2,\n \"uuid\": \"#2\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"#1\",\n \"short_id\": \"#8\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", + "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": #2,\n \"uuid\": \"#2\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"#1\",\n \"short_id\": \"#4\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", "contentType": "application/json; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=1073741824; expire=0" @@ -751,6 +765,11 @@ "body": { "customer": { "createdAt": "", + "credentials": { + "clientId": "#3", + "relayPassword": "#7", + "shortId": "#8" + }, "deviceLimit": 0, "expiresAt": null, "group": "", @@ -869,7 +888,7 @@ }, "private_key": "${REALITY_PRIVATE:reality-#3}", "short_id": [ - "#10" + "#8" ] }, "server_name": "www.amd.com" @@ -878,7 +897,7 @@ "users": [ { "name": "ng:#3", - "uuid": "#4" + "uuid": "#3" } ] } @@ -946,7 +965,7 @@ }, "private_key": "${REALITY_PRIVATE:reality-#3}", "short_id": [ - "#10" + "#8" ] }, "server_name": "www.amd.com" @@ -955,7 +974,7 @@ "users": [ { "name": "ng:#3", - "uuid": "#4" + "uuid": "#3" } ] } @@ -994,7 +1013,7 @@ "body": { "nodeClients": [ { - "clientUri": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", + "clientUri": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "createdAt": "", "customerId": "#1", "id": "#1", @@ -1003,7 +1022,7 @@ "updatedAt": "" }, { - "clientUri": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", + "clientUri": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "createdAt": "", "customerId": "#2", "id": "#2", @@ -1021,7 +1040,7 @@ "updatedAt": "" }, { - "clientUri": "vless://#4@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#10&type=tcp&headerType=none#N3", + "clientUri": "vless://#3@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#8&type=tcp&headerType=none#N3", "createdAt": "", "customerId": "#3", "id": "#4", @@ -1081,7 +1100,7 @@ "epoch": "diff-epoch-1", "uplink": 4000 }, - "wsPath": "#1" + "wsPath": "#5" }, { "artifacts": {}, @@ -1119,7 +1138,7 @@ "topology": "direct", "updatedAt": "", "usageCounters": null, - "wsPath": "#2" + "wsPath": "#6" }, { "artifacts": { @@ -1172,43 +1191,43 @@ }, "carolSubscriptions": { "auto-clash": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: #4\n type: \"vless\"\n uuid: \"#4\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#2\"\n short-id: \"#10\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N3\"\nrules:\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: #4\n type: \"vless\"\n uuid: \"#3\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#2\"\n short-id: \"#8\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N3\"\nrules:\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=0; expire=0" }, "auto-generic": { - "body": "vless://#4@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#10&type=tcp&headerType=none#N3\n", + "body": "vless://#3@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#8&type=tcp&headerType=none#N3\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=0; expire=0" }, "base64": { - "body": "vless://#4@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#10&type=tcp&headerType=none#N3\n", + "body": "vless://#3@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#8&type=tcp&headerType=none#N3\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=0; expire=0" }, "clash": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: #4\n type: \"vless\"\n uuid: \"#4\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#2\"\n short-id: \"#10\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N3\"\nrules:\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: #4\n type: \"vless\"\n uuid: \"#3\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#2\"\n short-id: \"#8\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N3\"\nrules:\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=0; expire=0" }, "clash-smart": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: #4\n type: \"vless\"\n uuid: \"#4\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#2\"\n short-id: \"#10\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"N3\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"N3\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N3\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N3\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N3\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: #4\n type: \"vless\"\n uuid: \"#3\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"#2\"\n short-id: \"#8\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"N3\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"N3\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N3\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N3\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"N3\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=0; expire=0" }, "raw": { - "body": "vless://#4@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#10&type=tcp&headerType=none#N3\n", + "body": "vless://#3@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#8&type=tcp&headerType=none#N3\n", "contentType": "text/plain; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=0; expire=0" }, "singbox": { - "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"s3.example.com\",\n \"server_port\": #4,\n \"uuid\": \"#4\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"#2\",\n \"short_id\": \"#10\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", + "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"s3.example.com\",\n \"server_port\": #4,\n \"uuid\": \"#3\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"#2\",\n \"short_id\": \"#8\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", "contentType": "application/json; charset=utf-8", "status": 200, "userinfo": "upload=0; download=0; total=0; expire=0" @@ -1286,6 +1305,11 @@ "customers": [ { "createdAt": "", + "credentials": { + "clientId": "#1", + "relayPassword": "#1", + "shortId": "#2" + }, "deviceLimit": 0, "expiresAt": null, "group": "", @@ -1329,6 +1353,11 @@ }, { "createdAt": "", + "credentials": { + "clientId": "#2", + "relayPassword": "#3", + "shortId": "#4" + }, "deviceLimit": 2, "expiresAt": null, "group": "", @@ -1353,6 +1382,11 @@ }, { "createdAt": "", + "credentials": { + "clientId": "#3", + "relayPassword": "#7", + "shortId": "#8" + }, "deviceLimit": 0, "expiresAt": null, "group": "", @@ -1441,7 +1475,7 @@ "epoch": "golden-epoch-1", "uplink": 8000 }, - "wsPath": "#1" + "wsPath": "#5" } }, "status": 200 @@ -1485,8 +1519,8 @@ }, "private_key": "${REALITY_PRIVATE:reality-#2}", "short_id": [ - "#4", - "#8" + "#2", + "#4" ] }, "server_name": "www.amd.com" @@ -1515,7 +1549,7 @@ "outbounds": [ { "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "server": "exit.example.com", "server_port": "#1", "tag": "ng-#2-exit", @@ -1573,8 +1607,8 @@ }, "private_key": "${REALITY_PRIVATE:reality-#2}", "short_id": [ - "#4", - "#8" + "#2", + "#4" ] }, "server_name": "www.amd.com" @@ -1603,7 +1637,7 @@ "outbounds": [ { "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "server": "exit.example.com", "server_port": "#1", "tag": "ng-#2-exit", @@ -1650,7 +1684,7 @@ "listen": "", "listen_port": "#1", "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "tag": "ng-#2-exit-in", "type": "shadowsocks" } @@ -1701,7 +1735,7 @@ "listen": "", "listen_port": "#1", "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "tag": "ng-#2-exit-in", "type": "shadowsocks" } @@ -1740,7 +1774,7 @@ "body": { "nodeClients": [ { - "clientUri": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed", + "clientUri": "vless://#1@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed", "createdAt": "", "customerId": "#1", "id": "#1", @@ -1749,7 +1783,7 @@ "updatedAt": "" }, { - "clientUri": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed", + "clientUri": "vless://#2@entry.example.com:#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#1&sid=#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed", "createdAt": "", "customerId": "#2", "id": "#2", @@ -1767,7 +1801,7 @@ "updatedAt": "" }, { - "clientUri": "vless://#4@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#10&type=tcp&headerType=none#N3", + "clientUri": "vless://#3@s3.example.com:#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=#2&sid=#8&type=tcp&headerType=none#N3", "createdAt": "", "customerId": "#3", "id": "#4", @@ -1834,7 +1868,7 @@ "epoch": "golden-epoch-1", "uplink": 8000 }, - "wsPath": "#1" + "wsPath": "#5" }, { "artifacts": {}, @@ -1874,7 +1908,7 @@ "topology": "direct", "updatedAt": "", "usageCounters": null, - "wsPath": "#2" + "wsPath": "#6" }, { "artifacts": { @@ -2272,6 +2306,11 @@ "customers": [ { "createdAt": "", + "credentials": { + "clientId": "#1", + "relayPassword": "#1", + "shortId": "#2" + }, "deviceLimit": 0, "expiresAt": null, "group": "", @@ -2309,6 +2348,11 @@ }, { "createdAt": "", + "credentials": { + "clientId": "#2", + "relayPassword": "#3", + "shortId": "#4" + }, "deviceLimit": 2, "expiresAt": null, "group": "", @@ -2327,6 +2371,11 @@ }, { "createdAt": "", + "credentials": { + "clientId": "#3", + "relayPassword": "#7", + "shortId": "#8" + }, "deviceLimit": 0, "expiresAt": null, "group": "", @@ -2406,8 +2455,8 @@ }, "private_key": "${REALITY_PRIVATE:reality-#2}", "short_id": [ - "#4", - "#8" + "#2", + "#4" ] }, "server_name": "www.amd.com" @@ -2436,7 +2485,7 @@ "outbounds": [ { "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "server": "exit.example.com", "server_port": "#1", "tag": "ng-#2-exit", @@ -2478,7 +2527,7 @@ "listen": "", "listen_port": "#1", "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "tag": "ng-#2-exit-in", "type": "shadowsocks" } @@ -2529,13 +2578,13 @@ "listen": "", "listen_port": "#3", "method": "2022-blake3-aes-128-gcm", - "password": "#12", + "password": "#11", "tag": "ng-#1-in", "type": "shadowsocks", "users": [ { "name": "ng:#1", - "password": "#5" + "password": "#12" } ] } @@ -2603,7 +2652,7 @@ }, "private_key": "${REALITY_PRIVATE:reality-#3}", "short_id": [ - "#10" + "#8" ] }, "server_name": "www.amd.com" @@ -2612,7 +2661,7 @@ "users": [ { "name": "ng:#3", - "uuid": "#4" + "uuid": "#3" } ] } @@ -2680,8 +2729,8 @@ }, "private_key": "${REALITY_PRIVATE:reality-#2}", "short_id": [ - "#4", - "#8" + "#2", + "#4" ] }, "server_name": "www.amd.com" @@ -2710,7 +2759,7 @@ "outbounds": [ { "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "server": "exit.example.com", "server_port": "#1", "tag": "ng-#2-exit", @@ -2752,7 +2801,7 @@ "listen": "", "listen_port": "#1", "method": "2022-blake3-aes-256-gcm", - "password": "#11", + "password": "#10", "tag": "ng-#2-exit-in", "type": "shadowsocks" } @@ -2800,11 +2849,6 @@ "nodeClients": [ { "createdAt": "", - "credentials": { - "clientId": "#1", - "relayPassword": "#3", - "shortId": "#4" - }, "customerId": "#1", "id": "#1", "nodeId": "#2", @@ -2813,11 +2857,6 @@ }, { "createdAt": "", - "credentials": { - "clientId": "#2", - "relayPassword": "#7", - "shortId": "#8" - }, "customerId": "#2", "id": "#2", "nodeId": "#2", @@ -2826,11 +2865,6 @@ }, { "createdAt": "", - "credentials": { - "clientId": "#3", - "relayPassword": "#5", - "shortId": "#6" - }, "customerId": "#1", "id": "#3", "nodeId": "#1", @@ -2839,11 +2873,6 @@ }, { "createdAt": "", - "credentials": { - "clientId": "#4", - "relayPassword": "#13", - "shortId": "#10" - }, "customerId": "#3", "id": "#4", "nodeId": "#3", @@ -2861,8 +2890,8 @@ "entryReady": true, "error": null, "exitCredentials": { - "clientId": "#5", - "password": "#11", + "clientId": "#4", + "password": "#10", "socksUser": "ng_5b8dd6b991" }, "exitPort": "#1", @@ -2909,7 +2938,7 @@ "epoch": "golden-epoch-1", "uplink": 8000 }, - "wsPath": "#1" + "wsPath": "#5" }, { "artifacts": {}, @@ -2938,12 +2967,12 @@ "retiredUsageEpochs": [], "serverId": "#1", "sortIndex": 1, - "ssServerKey": "#12", + "ssServerKey": "#11", "status": "active", "topology": "direct", "updatedAt": "", "usageCounters": null, - "wsPath": "#2" + "wsPath": "#6" }, { "artifacts": { diff --git a/panel-go/handlers.go b/panel-go/handlers.go index 68a3399..7129115 100644 --- a/panel-go/handlers.go +++ b/panel-go/handlers.go @@ -603,9 +603,40 @@ func CustomerSummary(data *Data, nowMs int64) map[string]any { } } -// credentialItems mirrors credentialItems in server.js. -func credentialItems(d *Data, customer *Customer) []map[string]any { - items := []map[string]any{} +// realityPublicKeyOf returns the node's panel-hosted Reality public key as +// reported back by the agent ("" when unknown). The keypair is the server's +// identity — it stays per-node even though customer credentials are shared. +func realityPublicKeyOf(node *Node) string { + if node == nil { + return "" + } + if raw, ok := node.Artifacts["realityPublicKey"]; ok && len(raw) > 0 { + var s string + if err := json.Unmarshal(raw, &s); err == nil { + return s + } + } + return "" +} + +// customerCredentialsView is the credentials-tab payload (round-2 item 2): +// the customer's ONE credential set, plus per-binding node facts. The +// Reality public key is shown per node because it belongs to the server, +// not to the customer. +func customerCredentialsView(d *Data, customer *Customer) map[string]any { + creds := map[string]any{"clientId": nil, "password": nil, "shortId": nil} + if customer.Credentials != nil { + if customer.Credentials.ClientID != "" { + creds["clientId"] = customer.Credentials.ClientID + } + if customer.Credentials.RelayPassword != "" { + creds["password"] = customer.Credentials.RelayPassword + } + if customer.Credentials.ShortID != "" { + creds["shortId"] = customer.Credentials.ShortID + } + } + bindings := []map[string]any{} for _, nc := range d.NodeClients { if nc.CustomerID != customer.ID { continue @@ -618,30 +649,26 @@ func credentialItems(d *Data, customer *Customer) []map[string]any { if node.Protocol == "socks5-auth" { username = NodeEmail(customer.ID) } - creds := nc.Credentials - var clientID, password, shortID any - if creds.ClientID != "" { - clientID = creds.ClientID + var publicKey any + if key := realityPublicKeyOf(node); key != "" { + publicKey = key } - if creds.RelayPassword != "" { - password = creds.RelayPassword - } - if creds.ShortID != "" { - shortID = creds.ShortID - } - items = append(items, map[string]any{ - "nodeClientId": nc.ID, - "nodeId": node.ID, - "nodeName": node.Name, - "protocol": node.Protocol, - "status": nc.Status, - "clientId": clientID, - "password": password, - "shortId": shortID, - "username": username, + bindings = append(bindings, map[string]any{ + "nodeClientId": nc.ID, + "nodeId": node.ID, + "nodeName": node.Name, + "protocol": node.Protocol, + "status": nc.Status, + "username": username, + "realityPublicKey": publicKey, }) } - return items + return map[string]any{ + "customerId": customer.ID, + "subscriptionToken": customer.SubscriptionToken, + "credentials": creds, + "bindings": bindings, + } } // CustomerBlockReason is customerBlockReason: "expired" / "traffic_limit" / @@ -1926,6 +1953,7 @@ func (a *App) handleAdminApi(w http.ResponseWriter, r *http.Request, pathname st Notes: CleanText(body["notes"], 1000), CreatedAt: NowISO(), UpdatedAt: NowISO(), + Credentials: func() *Credentials { c := NewCustomerCredentials(); return &c }(), } // Arm the monthly-reset schedule: it starts with the next // reset instant, it never fires retroactively at creation. @@ -2131,54 +2159,44 @@ func (a *App) handleAdminApi(w http.ResponseWriter, r *http.Request, pathname st if customer == nil { return true, httpErr(404, "客户不存在") } - SendJSON(w, 200, map[string]any{ - "customerId": customer.ID, - "subscriptionToken": customer.SubscriptionToken, - "items": credentialItems(data, customer), - }, nil) + SendJSON(w, 200, customerCredentialsView(data, customer), nil) return true, nil } - if params, ok := RoutePattern("/api/customers/:id/credentials/:ncid/regenerate", pathname); ok && r.Method == http.MethodPost { - var responseItem map[string]any + if params, ok := RoutePattern("/api/customers/:id/credentials/regenerate", pathname); ok && r.Method == http.MethodPost { + var view map[string]any _, err := a.store.Transaction(func(d *Data) (any, error) { customer := findCustomer(d, params["id"]) if customer == nil { return nil, httpErr(404, "客户不存在") } - var binding *NodeClient + // One set for the whole customer: rotating it rotates every + // node at once, and every deployed node is rebuilt so the new + // users land on the agents. + creds := NewCustomerCredentials() + customer.Credentials = &creds + customer.UpdatedAt = NowISO() + rebuilt := map[string]bool{} for _, nc := range d.NodeClients { - if nc.ID == params["ncid"] && nc.CustomerID == customer.ID { - binding = nc - break + if nc.CustomerID != customer.ID || nc.Status != "active" { + continue } - } - if binding == nil { - return nil, httpErr(404, "凭据不存在") - } - node := findNode(d, binding.NodeID) - if node == nil { - return nil, httpErr(404, "节点不存在") - } - binding.Credentials = NewNodeClientCredentials(node.Protocol) - binding.UpdatedAt = NowISO() - if node.Status != "draft" && node.Status != "removing" { + node := findNode(d, nc.NodeID) + if node == nil || node.Status == "draft" || node.Status == "removing" || rebuilt[node.ID] { + continue + } + rebuilt[node.ID] = true if err := RebuildNode(d, node, false); err != nil { return nil, err } } - Audit(d, actor, "regenerate_credentials", customer.ID, map[string]any{"nodeId": node.ID, "node": node.Name}) - for _, entry := range credentialItems(d, customer) { - if entry["nodeClientId"] == binding.ID { - responseItem = entry - break - } - } + Audit(d, actor, "regenerate_credentials", customer.ID, map[string]any{"nodesRebuilt": len(rebuilt)}) + view = customerCredentialsView(d, customer) return nil, nil }) if err != nil { return true, err } - SendJSON(w, 200, map[string]any{"item": responseItem}, nil) + SendJSON(w, 200, view, nil) return true, nil } if params, ok := RoutePattern("/api/customers/:id/external-sources/:sid/refresh", pathname); ok && r.Method == http.MethodPost { @@ -2437,21 +2455,12 @@ func (a *App) handleAdminApi(w http.ResponseWriter, r *http.Request, pathname st oldServerID := item.ServerID oldExitServerID := strVal(item.ExitServerID) oldForward := forwardNode(item) - oldProtocol := item.Protocol next, err := NormalizeNode(body, item, d) if err != nil { return nil, err } *item = *next EnsureNodeSecrets(item) - if item.Protocol != oldProtocol { - for _, nc := range d.NodeClients { - if nc.NodeID == item.ID { - nc.Credentials = NewNodeClientCredentials(item.Protocol) - nc.UpdatedAt = NowISO() - } - } - } if item.ServerID != oldServerID { QueueJob(d, oldServerID, item.ID, "delete_resource", &JobPayload{ResourceID: item.ResourceID, Kind: "node"}) item.EntryReady = false diff --git a/panel-go/orchestrator.go b/panel-go/orchestrator.go index f8f5d98..134917d 100644 --- a/panel-go/orchestrator.go +++ b/panel-go/orchestrator.go @@ -239,6 +239,16 @@ func payloadWithResource(resource map[string]any, kind string, keyMaterial map[s return payload } +// ensureCustomerCredentials fills a missing customer-level credential set +// (defensive: the store-load migration and customer creation provide one). +func ensureCustomerCredentials(d *Data, customerID string) { + customer := findCustomer(d, customerID) + if customer != nil && (customer.Credentials == nil || customer.Credentials.ClientID == "") { + creds := NewCustomerCredentials() + customer.Credentials = &creds + } +} + func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any, error) { server := orchFindServer(d, node.ServerID) if server == nil { @@ -262,11 +272,26 @@ func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any if clients == nil { clients = activeNodeClients(d, node) } + // Credentials live on the customer (round-2 item 2); stored bindings + // carry none. Stamp each customer's set onto throwaway copies for the + // resource builder so the stored records stay clean. + resolved := make([]*NodeClient, len(clients)) + for i, nc := range clients { + ensureCustomerCredentials(d, nc.CustomerID) + cp := *nc + if customer := findCustomer(d, nc.CustomerID); customer != nil && customer.Credentials != nil { + creds := *customer.Credentials + cp.Credentials = &creds + } else { + cp.Credentials = nil + } + resolved[i] = &cp + } return BuildNodeResource(BuildNodeResourceParams{ ResourceID: node.ResourceID, TagPrefix: tagForNode(node), Node: node, - NodeClients: clients, + NodeClients: resolved, Server: server, BindAddress: bindAddress, ExitServer: exitServer, @@ -977,14 +1002,14 @@ func assignCustomerData(d *Data, nodeID, customerID, actor string) (*NodeClient, nc.UpdatedAt = NowISO() } else { now := NowISO() + ensureCustomerCredentials(d, customerID) nc = &NodeClient{ - ID: NewID("ncl"), - NodeID: nodeID, - CustomerID: customerID, - Credentials: NewNodeClientCredentials(node.Protocol), - Status: "active", - CreatedAt: now, - UpdatedAt: now, + ID: NewID("ncl"), + NodeID: nodeID, + CustomerID: customerID, + Status: "active", + CreatedAt: now, + UpdatedAt: now, } d.NodeClients = append(d.NodeClients, nc) } @@ -1077,14 +1102,14 @@ func AlignCustomerNodes(d *Data, customer *Customer, desiredIds []string, actor status = "active" } now := NowISO() + ensureCustomerCredentials(d, customer.ID) d.NodeClients = append(d.NodeClients, &NodeClient{ - ID: NewID("ncl"), - NodeID: nodeID, - CustomerID: customer.ID, - Credentials: NewNodeClientCredentials(node.Protocol), - Status: status, - CreatedAt: now, - UpdatedAt: now, + ID: NewID("ncl"), + NodeID: nodeID, + CustomerID: customer.ID, + Status: status, + CreatedAt: now, + UpdatedAt: now, }) Audit(d, actor, "assign_customer", node.ID, map[string]any{"customerId": customer.ID, "customer": customer.Name}) if status == "active" { diff --git a/panel-go/protocols.go b/panel-go/protocols.go index 6336bdf..142cc8d 100644 --- a/panel-go/protocols.go +++ b/panel-go/protocols.go @@ -196,18 +196,47 @@ func (q xbQuery) String() string { func NodeEmail(customerID string) string { return "ng:" + customerID } -func NewNodeClientCredentials(protocol string) Credentials { - keyBytes := 24 - if cipher, ok := ShadowsocksMethods[protocol]; ok { - keyBytes = cipher.KeyBytes - } +// NewCustomerCredentials mints the single credential set a customer uses on +// every node (round-2 item 2, 3x-ui model). The relay password is always a +// base64 32-byte root key: SS2022 user keys are derived from it per method +// (see EffectiveRelayPassword), so one set serves aes128 and aes256 nodes +// alike; every other protocol uses it verbatim. +func NewCustomerCredentials() Credentials { return Credentials{ ClientID: NewUUID(), - RelayPassword: b64(keyBytes), + RelayPassword: b64(32), ShortID: shortId(), } } +// EffectiveRelayPassword maps a customer's relay password onto a node's +// protocol. SS2022 methods require the user key to be base64 of exactly the +// method's key length, so the user key is the deterministic prefix +// truncation of the 32-byte root key. Legacy SS methods and all other +// protocols take the password verbatim. +func EffectiveRelayPassword(protocol, relayPassword string) string { + cipher, ok := ShadowsocksMethods[protocol] + if !ok || !strings.HasPrefix(cipher.Method, "2022-blake3-") { + return relayPassword + } + raw, err := base64.StdEncoding.DecodeString(relayPassword) + if err != nil || len(raw) < cipher.KeyBytes { + return relayPassword + } + return base64.StdEncoding.EncodeToString(raw[:cipher.KeyBytes]) +} + +// clientCredentials resolves the credentials stamped onto a binding for +// resource building. Stored bindings no longer carry credentials (they live +// on the customer); builders receive resolved copies, and this accessor +// keeps a nil hole from becoming a panic. +func clientCredentials(nc *NodeClient) Credentials { + if nc == nil || nc.Credentials == nil { + return Credentials{} + } + return *nc.Credentials +} + func EnsureNodeSecrets(node *Node) { if node.WSPath == nil || *node.WSPath == "" { node.WSPath = StrPtr("/" + hex.EncodeToString(xbRandBytes(9))) @@ -352,12 +381,13 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv users := make([]any, 0, len(nodeClients)) shortIDs := make([]any, 0, len(nodeClients)) for _, nc := range nodeClients { - user := map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": nc.Credentials.ClientID} + creds := clientCredentials(nc) + user := map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": creds.ClientID} if protocol == "vless-reality-vision" { user["flow"] = "xtls-rprx-vision" } users = append(users, user) - shortIDs = append(shortIDs, nc.Credentials.ShortID) + shortIDs = append(shortIDs, creds.ShortID) } destPort := node.Reality.DestPort if destPort == 0 { @@ -383,7 +413,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv } users := make([]any, 0, len(nodeClients)) for _, nc := range nodeClients { - users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": nc.Credentials.ClientID}) + users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": clientCredentials(nc).ClientID}) } wsPath := xbDeref(node.WSPath) if wsPath == "" { @@ -406,7 +436,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv if cipher, ok := ShadowsocksMethods[protocol]; ok { users := make([]any, 0, len(nodeClients)) for _, nc := range nodeClients { - users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword}) + users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": EffectiveRelayPassword(protocol, clientCredentials(nc).RelayPassword)}) } return map[string]any{ "type": "shadowsocks", "tag": tag, "listen": listen, "listen_port": port, @@ -417,7 +447,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv if protocol == "socks5-auth" { users := make([]any, 0, len(nodeClients)) for _, nc := range nodeClients { - users = append(users, map[string]any{"username": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword}) + users = append(users, map[string]any{"username": NodeEmail(nc.CustomerID), "password": clientCredentials(nc).RelayPassword}) } return map[string]any{"type": "socks", "tag": tag, "listen": listen, "listen_port": port, "users": users}, nil } @@ -428,7 +458,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv } users := make([]any, 0, len(nodeClients)) for _, nc := range nodeClients { - users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword}) + users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": clientCredentials(nc).RelayPassword}) } return map[string]any{ "type": "hysteria2", "tag": tag, "listen": listen, "listen_port": port, "users": users, @@ -446,7 +476,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv } users := make([]any, 0, len(nodeClients)) for _, nc := range nodeClients { - users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword}) + users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": clientCredentials(nc).RelayPassword}) } return map[string]any{ "type": "anytls", "tag": tag, "listen": listen, "listen_port": port, "users": users, @@ -682,9 +712,9 @@ func BuildClientURI(p BuildClientURIParams) (string, bool) { return fmt.Sprintf("vless://%s@%s:%d?%s#%s", p.ClientID, host, p.RelayPort, q.String(), label), true } if cipher, ok := ShadowsocksMethods[protocol]; ok { - password := p.RelayPassword + password := EffectiveRelayPassword(protocol, p.RelayPassword) if strings.HasPrefix(cipher.Method, "2022-blake3-") && p.SSServerKey != "" { - password = p.SSServerKey + ":" + p.RelayPassword + password = p.SSServerKey + ":" + password } user := base64.RawURLEncoding.EncodeToString([]byte(cipher.Method + ":" + password)) return fmt.Sprintf("ss://%s@%s:%d#%s", user, host, p.RelayPort, label), true diff --git a/panel-go/protocols_test.go b/panel-go/protocols_test.go index 8d945c4..0a2675e 100644 --- a/panel-go/protocols_test.go +++ b/panel-go/protocols_test.go @@ -83,8 +83,8 @@ func TestGoldenResources(t *testing.T) { ExitCredentials: &ExitCredentials{ClientID: "11111111-2222-3333-4444-0000000000ff", Password: exitPassword, SocksUser: "ng_0123456789"}, } rfClients := []*NodeClient{ - {NodeID: "nod_rf", CustomerID: "cus_1", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "unused1", ShortID: "abcd0001ef01"}}, - {NodeID: "nod_rf", CustomerID: "cus_2", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000002", RelayPassword: "unused2", ShortID: "abcd0002ef01"}}, + {NodeID: "nod_rf", CustomerID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "unused1", ShortID: "abcd0001ef01"}}, + {NodeID: "nod_rf", CustomerID: "cus_2", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000002", RelayPassword: "unused2", ShortID: "abcd0002ef01"}}, } entryServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.10"} exitServer := &Server{ID: "srv_exit", PublicAddress: "exit.example.com"} @@ -102,7 +102,7 @@ func TestGoldenResources(t *testing.T) { SSServerKey: StrPtr("CwwNDg8QERITFBUWFxgZGg=="), } ssClients := []*NodeClient{ - {NodeID: "nod_ss", CustomerID: "cus_1", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01"}}, + {NodeID: "nod_ss", CustomerID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01"}}, } ssServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.11"} diff --git a/panel-go/store.go b/panel-go/store.go index 6bbf503..9deb053 100644 --- a/panel-go/store.go +++ b/panel-go/store.go @@ -6,6 +6,7 @@ package main // the same serialization here. import ( + "encoding/base64" "errors" "fmt" "os" @@ -133,10 +134,70 @@ func (s *Store) Init() error { if err := validateData(&data); err != nil { return err } + if MigrateCustomerCredentials(&data) { + // Persist the converged credential model immediately, so a crash + // before the next transaction cannot resurrect per-binding sets. + if err := s.persist(&data); err != nil { + return err + } + } s.Data = &data return nil } +// MigrateCustomerCredentials converges legacy per-binding credentials into +// each customer's single set (round-2 item 2; the 3x-ui model of one +// credential row per client). Deterministic rules: +// - A customer with a complete stored set keeps it. +// - Otherwise the set is copied from the customer's first binding (in +// stored order) that carries credentials; a customer with no usable +// binding credentials gets a fresh set. +// - The relay password must be a base64 32-byte root key, because SS2022 +// user keys derive from it by prefix truncation (EffectiveRelayPassword). +// If the converged password does not decode to exactly 32 bytes, only +// the password is regenerated — clientId and shortId are preserved. +// - Every binding's stored credentials are then cleared, so bindings stop +// carrying credentials at all. +// +// Returns true when anything changed. +func MigrateCustomerCredentials(d *Data) bool { + changed := false + for _, customer := range d.Customers { + var creds *Credentials + if customer.Credentials != nil && customer.Credentials.ClientID != "" { + copied := *customer.Credentials + creds = &copied + } else { + for _, nc := range d.NodeClients { + if nc.CustomerID == customer.ID && nc.Credentials != nil && nc.Credentials.ClientID != "" { + copied := *nc.Credentials + creds = &copied + break + } + } + } + if creds == nil { + fresh := NewCustomerCredentials() + creds = &fresh + } + if raw, err := base64.StdEncoding.DecodeString(creds.RelayPassword); err != nil || len(raw) != 32 { + creds.RelayPassword = b64(32) + } + old := customer.Credentials + if old == nil || old.ClientID != creds.ClientID || old.RelayPassword != creds.RelayPassword || old.ShortID != creds.ShortID { + customer.Credentials = creds + changed = true + } + } + for _, nc := range d.NodeClients { + if nc.Credentials != nil { + nc.Credentials = nil + changed = true + } + } + return changed +} + // Snapshot returns a deep copy of the current data (JS structuredClone). func (s *Store) Snapshot() *Data { s.mu.Lock() diff --git a/panel-go/subscriptions.go b/panel-go/subscriptions.go index 44b8d4f..3ffad3a 100644 --- a/panel-go/subscriptions.go +++ b/panel-go/subscriptions.go @@ -108,13 +108,17 @@ func NodeClientURI(d *Data, nc *NodeClient) string { if networkMode == "" { networkMode = "ipv4" } + creds := Credentials{} + if customer := findCustomer(d, nc.CustomerID); customer != nil && customer.Credentials != nil { + creds = *customer.Credentials + } uri, ok := BuildClientURI(BuildClientURIParams{ Protocol: node.Protocol, RelayServer: server, RelayPort: node.Port, - ClientID: nc.Credentials.ClientID, - RelayPassword: nc.Credentials.RelayPassword, - ShortID: nc.Credentials.ShortID, + ClientID: creds.ClientID, + RelayPassword: creds.RelayPassword, + ShortID: creds.ShortID, WSPath: xbDeref(node.WSPath), SocksUser: NodeEmail(nc.CustomerID), Reality: node.Reality, diff --git a/panel-go/subscriptions_test.go b/panel-go/subscriptions_test.go index 3b92cfb..b902a77 100644 --- a/panel-go/subscriptions_test.go +++ b/panel-go/subscriptions_test.go @@ -36,26 +36,25 @@ func goldenSubData() *Data { Reality: &RealityConfig{KeyID: "reality-nod_rv", ServerName: "www.apple.com", DestPort: 443}, Artifacts: art("FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu")}, } - nc := func(nodeID, clientID, relayPassword, shortID string) *NodeClient { - return &NodeClient{ - NodeID: nodeID, CustomerID: "cus_1", Status: "active", - Credentials: Credentials{ClientID: clientID, RelayPassword: relayPassword, ShortID: shortID}, - } + nc := func(nodeID string) *NodeClient { + return &NodeClient{NodeID: nodeID, CustomerID: "cus_1", Status: "active"} } clients := []*NodeClient{ - nc("nod_rf", "11111111-2222-3333-4444-000000000001", "unused", "abcd0001ef01"), - nc("nod_ss", "11111111-2222-3333-4444-000000000003", "FRYXGBkaGxwdHh8gISIjJA==", "abcd0003ef01"), - nc("nod_hy", "11111111-2222-3333-4444-000000000004", "HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz", "abcd0004ef01"), - nc("nod_any", "11111111-2222-3333-4444-000000000005", "IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6", "abcd0005ef01"), - nc("nod_ws", "11111111-2222-3333-4444-000000000006", "unused", "abcd0006ef01"), - nc("nod_sk", "11111111-2222-3333-4444-000000000007", "MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI", "abcd0007ef01"), - nc("nod_rv", "11111111-2222-3333-4444-000000000008", "unused", "abcd0008ef01"), + nc("nod_rf"), nc("nod_ss"), nc("nod_hy"), nc("nod_any"), + nc("nod_ws"), nc("nod_sk"), nc("nod_rv"), } return &Data{ Servers: []*Server{entryServer, v6Server}, Nodes: nodes, NodeClients: clients, - Customers: []*Customer{{ID: "cus_1"}}, + // One credential set for the whole customer (round-2 item 2). The + // values are the old RF binding's uuid/shortId and the old SS + // binding's password, so the RF and SS entries are byte-stable + // against the previous golden file; every other entry now carries + // this same set. + Customers: []*Customer{{ID: "cus_1", Credentials: &Credentials{ + ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01", + }}}, } } diff --git a/panel-go/testdata/golden_subscriptions.json b/panel-go/testdata/golden_subscriptions.json index 3eff545..c3d5651 100644 --- a/panel-go/testdata/golden_subscriptions.json +++ b/panel-go/testdata/golden_subscriptions.json @@ -2,7 +2,7 @@ "entries": [ { "nodeId": "nod_rf", - "clientUri": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu&sid=abcd0001ef01&type=tcp&headerType=none&flow=xtls-rprx-vision#RF-Reality" + "clientUri": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none\u0026security=reality\u0026sni=www.amd.com\u0026fp=chrome\u0026pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none\u0026flow=xtls-rprx-vision#RF-Reality" }, { "nodeId": "nod_ss", @@ -10,56 +10,56 @@ }, { "nodeId": "nod_hy", - "clientUri": "hysteria2://HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz@entry.example.com:23459?sni=tls.example.com&alpn=h3#HY2" + "clientUri": "hysteria2://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23459?sni=tls.example.com\u0026alpn=h3#HY2" }, { "nodeId": "nod_any", - "clientUri": "anytls://IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6@entry.example.com:23460?sni=tls.example.com#AnyTLS" + "clientUri": "anytls://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23460?sni=tls.example.com#AnyTLS" }, { "nodeId": "nod_ws", - "clientUri": "vless://11111111-2222-3333-4444-000000000006@entry.example.com:23461?encryption=none&security=tls&type=ws&path=%2Fwspath123&sni=tls.example.com#WS-TLS" + "clientUri": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23461?encryption=none\u0026security=tls\u0026type=ws\u0026path=%2Fwspath123\u0026sni=tls.example.com#WS-TLS" }, { "nodeId": "nod_sk", - "clientUri": "socks5://ng%3Acus_1:MTIzNDU2Nzg5Ojs8PT4%2FQEFCQ0RFRkdI@entry.example.com:23462#Socks" + "clientUri": "socks5://ng%3Acus_1:FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23462#Socks" }, { "nodeId": "nod_rv", - "clientUri": "vless://11111111-2222-3333-4444-000000000008@v6.example.com:23463?encryption=none&security=reality&sni=www.apple.com&fp=chrome&pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu&sid=abcd0008ef01&type=tcp&headerType=none#RV-Plain" + "clientUri": "vless://11111111-2222-3333-4444-000000000001@v6.example.com:23463?encryption=none\u0026security=reality\u0026sni=www.apple.com\u0026fp=chrome\u0026pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none#RV-Plain" } ], "formats": { - "raw": { - "body": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu&sid=abcd0001ef01&type=tcp&headerType=none&flow=xtls-rprx-vision#RF-Reality\nss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Q3d3TkRnOFFFUklURkJVV0Z4Z1pHZz09OkZSWVhHQmthR3h3ZEhoOGdJU0lqSkE9PQ@entry.example.com:23458#SS-Direct\nhysteria2://HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz@entry.example.com:23459?sni=tls.example.com&alpn=h3#HY2\nanytls://IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6@entry.example.com:23460?sni=tls.example.com#AnyTLS\nvless://11111111-2222-3333-4444-000000000006@entry.example.com:23461?encryption=none&security=tls&type=ws&path=%2Fwspath123&sni=tls.example.com#WS-TLS\nsocks5://ng%3Acus_1:MTIzNDU2Nzg5Ojs8PT4%2FQEFCQ0RFRkdI@entry.example.com:23462#Socks\nvless://11111111-2222-3333-4444-000000000008@v6.example.com:23463?encryption=none&security=reality&sni=www.apple.com&fp=chrome&pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu&sid=abcd0008ef01&type=tcp&headerType=none#RV-Plain\n", - "contentType": "text/plain; charset=utf-8" - }, - "base64": { - "body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vSEIwZUh5QWhJaU1rSlNZbktDa3FLeXd0TGk4d01USXpAZW50cnkuZXhhbXBsZS5jb206MjM0NTk/c25pPXRscy5leGFtcGxlLmNvbSZhbHBuPWgzI0hZMgphbnl0bHM6Ly9JeVFsSmljb0tTb3JMQzB1THpBeE1qTTBOVFkzT0RrNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MD9zbmk9dGxzLmV4YW1wbGUuY29tI0FueVRMUwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MT9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9dGxzJnR5cGU9d3MmcGF0aD0lMkZ3c3BhdGgxMjMmc25pPXRscy5leGFtcGxlLmNvbSNXUy1UTFMKc29ja3M1Oi8vbmclM0FjdXNfMTpNVEl6TkRVMk56ZzVPanM4UFQ0JTJGUUVGQ1EwUkZSa2RJQGVudHJ5LmV4YW1wbGUuY29tOjIzNDYyI1NvY2tzCnZsZXNzOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtMDAwMDAwMDAwMDA4QHY2LmV4YW1wbGUuY29tOjIzNDYzP2VuY3J5cHRpb249bm9uZSZzZWN1cml0eT1yZWFsaXR5JnNuaT13d3cuYXBwbGUuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSdktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDA4ZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUjUlYtUGxhaW4K", - "contentType": "text/plain; charset=utf-8" - }, - "clash": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000006\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000008\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0008ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n", - "contentType": "text/yaml; charset=utf-8" - }, - "clash-smart": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000006\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000008\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0008ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", - "contentType": "text/yaml; charset=utf-8" - }, - "singbox": { - "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\",\n \"node-2\",\n \"node-3\",\n \"node-4\",\n \"node-5\",\n \"node-6\",\n \"node-7\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23456,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0001ef01\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23458,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n },\n {\n \"type\": \"hysteria2\",\n \"tag\": \"node-3\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23459,\n \"password\": \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\",\n \"alpn\": [\n \"h3\"\n ]\n }\n },\n {\n \"type\": \"anytls\",\n \"tag\": \"node-4\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23460,\n \"password\": \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-5\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23461,\n \"uuid\": \"11111111-2222-3333-4444-000000000006\",\n \"transport\": {\n \"type\": \"ws\",\n \"path\": \"/wspath123\"\n },\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"socks\",\n \"tag\": \"node-6\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23462,\n \"version\": \"5\",\n \"username\": \"ng:cus_1\",\n \"password\": \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-7\",\n \"server\": \"v6.example.com\",\n \"server_port\": 23463,\n \"uuid\": \"11111111-2222-3333-4444-000000000008\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.apple.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0008ef01\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", - "contentType": "application/json; charset=utf-8" - }, - "surge": { - "body": "[General]\nloglevel = notify\n[Proxy]\nSS-Direct = ss, entry.example.com, 23458, encrypt-method=2022-blake3-aes-128-gcm, password=CwwNDg8QERITFBUWFxgZGg__:FRYXGBkaGxwdHh8gISIjJA__\nSocks = socks5, entry.example.com, 23462, ng:cus_1, MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\n[Proxy Group]\n节点选择 = select, SS-Direct, Socks, DIRECT\n自动选择 = url-test, SS-Direct, Socks, url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n", - "contentType": "text/plain; charset=utf-8" - }, "auto-clash-ua": { - "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000006\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000008\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0008ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n", + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n", "contentType": "text/yaml; charset=utf-8" }, "auto-plain-ua": { - "body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vSEIwZUh5QWhJaU1rSlNZbktDa3FLeXd0TGk4d01USXpAZW50cnkuZXhhbXBsZS5jb206MjM0NTk/c25pPXRscy5leGFtcGxlLmNvbSZhbHBuPWgzI0hZMgphbnl0bHM6Ly9JeVFsSmljb0tTb3JMQzB1THpBeE1qTTBOVFkzT0RrNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MD9zbmk9dGxzLmV4YW1wbGUuY29tI0FueVRMUwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MT9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9dGxzJnR5cGU9d3MmcGF0aD0lMkZ3c3BhdGgxMjMmc25pPXRscy5leGFtcGxlLmNvbSNXUy1UTFMKc29ja3M1Oi8vbmclM0FjdXNfMTpNVEl6TkRVMk56ZzVPanM4UFQ0JTJGUUVGQ1EwUkZSa2RJQGVudHJ5LmV4YW1wbGUuY29tOjIzNDYyI1NvY2tzCnZsZXNzOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtMDAwMDAwMDAwMDA4QHY2LmV4YW1wbGUuY29tOjIzNDYzP2VuY3J5cHRpb249bm9uZSZzZWN1cml0eT1yZWFsaXR5JnNuaT13d3cuYXBwbGUuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSdktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDA4ZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUjUlYtUGxhaW4K", + "body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vRlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OT9zbmk9dGxzLmV4YW1wbGUuY29tJmFscG49aDMjSFkyCmFueXRsczovL0ZSWVhHQmthR3h3ZEhoOGdJU0lqSkElM0QlM0RAZW50cnkuZXhhbXBsZS5jb206MjM0NjA/c25pPXRscy5leGFtcGxlLmNvbSNBbnlUTFMKdmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NjE/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXRscyZ0eXBlPXdzJnBhdGg9JTJGd3NwYXRoMTIzJnNuaT10bHMuZXhhbXBsZS5jb20jV1MtVExTCnNvY2tzNTovL25nJTNBY3VzXzE6RlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MiNTb2Nrcwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwMUB2Ni5leGFtcGxlLmNvbToyMzQ2Mz9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9cmVhbGl0eSZzbmk9d3d3LmFwcGxlLmNvbSZmcD1jaHJvbWUmcGJrPUZJWEVEUEJLUnZLZXkwMTIzNDU2Nzg5YWJjZGVmZ2hpamtsbW5vcHFyc3R1JnNpZD1hYmNkMDAwMWVmMDEmdHlwZT10Y3AmaGVhZGVyVHlwZT1ub25lI1JWLVBsYWluCg==", + "contentType": "text/plain; charset=utf-8" + }, + "base64": { + "body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vRlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OT9zbmk9dGxzLmV4YW1wbGUuY29tJmFscG49aDMjSFkyCmFueXRsczovL0ZSWVhHQmthR3h3ZEhoOGdJU0lqSkElM0QlM0RAZW50cnkuZXhhbXBsZS5jb206MjM0NjA/c25pPXRscy5leGFtcGxlLmNvbSNBbnlUTFMKdmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NjE/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXRscyZ0eXBlPXdzJnBhdGg9JTJGd3NwYXRoMTIzJnNuaT10bHMuZXhhbXBsZS5jb20jV1MtVExTCnNvY2tzNTovL25nJTNBY3VzXzE6RlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MiNTb2Nrcwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwMUB2Ni5leGFtcGxlLmNvbToyMzQ2Mz9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9cmVhbGl0eSZzbmk9d3d3LmFwcGxlLmNvbSZmcD1jaHJvbWUmcGJrPUZJWEVEUEJLUnZLZXkwMTIzNDU2Nzg5YWJjZGVmZ2hpamtsbW5vcHFyc3R1JnNpZD1hYmNkMDAwMWVmMDEmdHlwZT10Y3AmaGVhZGVyVHlwZT1ub25lI1JWLVBsYWluCg==", + "contentType": "text/plain; charset=utf-8" + }, + "clash": { + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n", + "contentType": "text/yaml; charset=utf-8" + }, + "clash-smart": { + "body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n", + "contentType": "text/yaml; charset=utf-8" + }, + "raw": { + "body": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none\u0026security=reality\u0026sni=www.amd.com\u0026fp=chrome\u0026pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none\u0026flow=xtls-rprx-vision#RF-Reality\nss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Q3d3TkRnOFFFUklURkJVV0Z4Z1pHZz09OkZSWVhHQmthR3h3ZEhoOGdJU0lqSkE9PQ@entry.example.com:23458#SS-Direct\nhysteria2://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23459?sni=tls.example.com\u0026alpn=h3#HY2\nanytls://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23460?sni=tls.example.com#AnyTLS\nvless://11111111-2222-3333-4444-000000000001@entry.example.com:23461?encryption=none\u0026security=tls\u0026type=ws\u0026path=%2Fwspath123\u0026sni=tls.example.com#WS-TLS\nsocks5://ng%3Acus_1:FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23462#Socks\nvless://11111111-2222-3333-4444-000000000001@v6.example.com:23463?encryption=none\u0026security=reality\u0026sni=www.apple.com\u0026fp=chrome\u0026pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none#RV-Plain\n", + "contentType": "text/plain; charset=utf-8" + }, + "singbox": { + "body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\",\n \"node-2\",\n \"node-3\",\n \"node-4\",\n \"node-5\",\n \"node-6\",\n \"node-7\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23456,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0001ef01\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23458,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n },\n {\n \"type\": \"hysteria2\",\n \"tag\": \"node-3\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23459,\n \"password\": \"FRYXGBkaGxwdHh8gISIjJA==\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\",\n \"alpn\": [\n \"h3\"\n ]\n }\n },\n {\n \"type\": \"anytls\",\n \"tag\": \"node-4\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23460,\n \"password\": \"FRYXGBkaGxwdHh8gISIjJA==\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-5\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23461,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"transport\": {\n \"type\": \"ws\",\n \"path\": \"/wspath123\"\n },\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"socks\",\n \"tag\": \"node-6\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23462,\n \"version\": \"5\",\n \"username\": \"ng:cus_1\",\n \"password\": \"FRYXGBkaGxwdHh8gISIjJA==\"\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-7\",\n \"server\": \"v6.example.com\",\n \"server_port\": 23463,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.apple.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0001ef01\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n", + "contentType": "application/json; charset=utf-8" + }, + "surge": { + "body": "[General]\nloglevel = notify\n[Proxy]\nSS-Direct = ss, entry.example.com, 23458, encrypt-method=2022-blake3-aes-128-gcm, password=CwwNDg8QERITFBUWFxgZGg__:FRYXGBkaGxwdHh8gISIjJA__\nSocks = socks5, entry.example.com, 23462, ng:cus_1, FRYXGBkaGxwdHh8gISIjJA__\n[Proxy Group]\n节点选择 = select, SS-Direct, Socks, DIRECT\n自动选择 = url-test, SS-Direct, Socks, url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n", "contentType": "text/plain; charset=utf-8" } } diff --git a/panel-go/types.go b/panel-go/types.go index 1a57e37..00c3630 100644 --- a/panel-go/types.go +++ b/panel-go/types.go @@ -560,9 +560,13 @@ type Customer struct { ServerUsage map[string]*UsageSlot `json:"serverUsage,omitempty"` LastUsageAt *string `json:"lastUsageAt,omitempty"` LastOnlineAt *string `json:"lastOnlineAt,omitempty"` - ExternalSources []ExtSource `json:"externalSources,omitempty"` - ManualLinks []ManualLink `json:"manualLinks,omitempty"` - Extra extraFields `json:"-"` + // The customer's single credential set, shared by all of their node + // bindings (round-2 item 2). Populated by the store-load migration for + // legacy records and at creation for new ones. + Credentials *Credentials `json:"credentials,omitempty"` + ExternalSources []ExtSource `json:"externalSources,omitempty"` + ManualLinks []ManualLink `json:"manualLinks,omitempty"` + Extra extraFields `json:"-"` } // ExtSource is one external subscription source on a customer @@ -781,14 +785,18 @@ func (c Credentials) MarshalJSON() ([]byte, error) { } type NodeClient struct { - ID string `json:"id"` - NodeID string `json:"nodeId"` - CustomerID string `json:"customerId"` - Credentials Credentials `json:"credentials"` - Status string `json:"status"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - Extra extraFields `json:"-"` + ID string `json:"id"` + NodeID string `json:"nodeId"` + CustomerID string `json:"customerId"` + // Deprecated: credentials live on the Customer (round-2 item 2). This + // field is only a migration input — the store-load migration converges + // it into Customer.Credentials and clears it, so it disappears from + // disk on the next save. Builders receive resolved copies instead. + Credentials *Credentials `json:"credentials,omitempty"` + Status string `json:"status"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + Extra extraFields `json:"-"` } func (n *NodeClient) UnmarshalJSON(raw []byte) error { diff --git a/panel-go/uireskin_test.go b/panel-go/uireskin_test.go index ec0f51c..6549cb9 100644 --- a/panel-go/uireskin_test.go +++ b/panel-go/uireskin_test.go @@ -53,8 +53,8 @@ func TestAggregateEntriesOrderAndDedup(t *testing.T) { }, Servers: []*Server{{ID: "srv_1", Name: "S", PublicAddress: "s.example.com"}}, NodeClients: []*NodeClient{ - {ID: "ncl_b", NodeID: "nod_b", CustomerID: "cus_1", Status: "active", Credentials: Credentials{RelayPassword: "pw-b"}}, - {ID: "ncl_a", NodeID: "nod_a", CustomerID: "cus_1", Status: "active", Credentials: Credentials{RelayPassword: "pw-a"}}, + {ID: "ncl_b", NodeID: "nod_b", CustomerID: "cus_1", Status: "active"}, + {ID: "ncl_a", NodeID: "nod_a", CustomerID: "cus_1", Status: "active"}, }, SubSourceCache: map[string]*SubSourceCacheEntry{ "src_1": {Items: []SubCacheItem{{URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Dup", Name: "Dup"}}}, @@ -63,12 +63,13 @@ func TestAggregateEntriesOrderAndDedup(t *testing.T) { okAt := NowISO() d.SubSourceCache["src_1"].OkAt = &okAt customer := &Customer{ - ID: "cus_1", + ID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "pw-a", ShortID: "abcd0001ef01"}, ManualLinks: []ManualLink{ {ID: "lnk_1", URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Whatever", Remark: "手动一号", Enabled: true}, }, ExternalSources: []ExtSource{{ID: "src_1", URL: "https://example.com/sub", Remark: "机场", Enabled: true}}, } + d.Customers = []*Customer{customer} entries := AggregateEntries(d, customer) names := []string{} for _, entry := range entries { diff --git a/public/app.js b/public/app.js index 868fa82..bc3dd98 100644 --- a/public/app.js +++ b/public/app.js @@ -636,19 +636,22 @@ function renderCredentialsPane(customer) { if (!customer) { pane.innerHTML = '
创建客户后生成凭据
'; return; } const data = customerFormState.credentials; if (!data) { pane.innerHTML = '
加载中…
'; return; } - const groups = data.items.map((entry) => `
-
${esc(entry.nodeName)}${esc(statusText[entry.status] || entry.status)}${esc(entry.protocol)} -
- ${credRowHTML('UUID', entry.clientId)} - ${credRowHTML('密码', entry.password)} - ${entry.shortId ? credRowHTML('Reality Short ID', entry.shortId) : ''} + const creds = data.credentials || {}; + const bindings = (data.bindings || []).map((entry) => `
+
${esc(entry.nodeName)}${esc(statusText[entry.status] || entry.status)}${esc(entry.protocol)}
${entry.username ? credRowHTML('用户名', entry.username) : ''} + ${entry.realityPublicKey ? credRowHTML('Reality 公钥', entry.realityPublicKey) : ''}
`).join(''); pane.innerHTML = `
订阅
订阅令牌${esc(data.subscriptionToken || '—')}
- ${groups || '
尚未关联节点
'}`; +
凭据(全部节点共用)
+ ${credRowHTML('UUID', creds.clientId)} + ${credRowHTML('密码', creds.password)} + ${credRowHTML('Reality Short ID', creds.shortId)} +
+ ${bindings || '
尚未关联节点
'}`; } function linkStatusLine(customer, source) { @@ -1147,11 +1150,10 @@ document.addEventListener('click', async (event) => { api(`/api/customers/${itemId}/credentials`).then((data) => { customerFormState.credentials = data; renderCredentialsPane(result.customer); }).catch(() => {}); } else if (modalOpen()) subscriptionModal(result.customer); } - else if (action === 'regen-credential') { + else if (action === 'regen-credentials') { const customerId = $('#customer-form').dataset.customerId; - if (!await confirmModal(`重新生成「${button.dataset.node}」的凭据?该客户在此节点的现有连接将立即失效,需要重新导入订阅。`, '重新生成')) return; - await api(`/api/customers/${customerId}/credentials/${button.dataset.ncid}/regenerate`, { method: 'POST', body: '{}' }); - const data = await api(`/api/customers/${customerId}/credentials`); + if (!await confirmModal('重新生成该客户的凭据?全部节点同时更换,现有连接将立即失效,需要重新导入订阅。', '重新生成')) return; + const data = await api(`/api/customers/${customerId}/credentials/regenerate`, { method: 'POST', body: '{}' }); customerFormState.credentials = data; renderCredentialsPane(customerById(customerId)); toast('凭据已重新生成');