feat: customer-level credentials shared across all nodes (item 2)

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
This commit is contained in:
aaaamoooo
2026-10-20 21:21:13 +00:00
parent bb8a0cb0c4
commit ad18e16ac6
16 changed files with 673 additions and 272 deletions
+72
View File
@@ -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/<branch>/<path>` | `git.881218.xyz/rogue/NexusGate/raw/branch/<branch>/<path>` |
| release | `github.com/aaaamoooo/NexusGate/releases/download/<tag>/<file>` | `git.881218.xyz/rogue/NexusGate/releases/download/<tag>/<file>` |
| archive | `github.com/<REPO>/archive/refs/heads/<branch>.tar.gz` | `git.881218.xyz/rogue/NexusGate/archive/<branch>.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,不受影响)。
+18
View File
@@ -72,3 +72,21 @@ amo 各页主内容区两侧留白偏大;把左右间距(padding/最大宽
## 10. 删除顶栏全局搜索框(2026-10-21 04:39,用户附顶栏截图) ## 10. 删除顶栏全局搜索框(2026-10-21 04:39,用户附顶栏截图)
顶栏"搜索服务器、客户、节点…"输入框是写死 disabled 的摆设、无任何功能;且每页都有自己的可用搜索框,此框多余——删除(顶栏共用一处,删一处全站生效)。 顶栏"搜索服务器、客户、节点…"输入框是写死 disabled 的摆设、无任何功能;且每页都有自己的可用搜索框,此框多余——删除(顶栏共用一处,删一处全站生效)。
## 11. 菜单与命名调整 + 页面合并(2026-10-21 04:47,用户附节点页全图)
- 左侧菜单「客户与订阅」→ 改名「用户订阅」(页面标题/面包屑同步)
- 左侧菜单「服务器」→ 改名「节点管理」
- 左侧菜单「转发与节点」→ 改名「入站&出站」(页面标题/面包屑同步)
- 该页「+ 新建节点」按钮 → 改为「添加入站」;页内计数文案「9 个节点」→「9 个入站」(同页其他"节点"计数文案同步对齐)
- 左侧菜单「设置与运维」→ 改名「设置」
- 「安装与命令」页面的全部内容合并进「设置」页面,合并后删除「安装与命令」页面(菜单项与路由一并摘除)
## 12. 删除系统状态页「最近活动」模块(2026-10-21 04:56,用户附截图)
首页(系统状态页)底部的「最近活动」面板整个删除(该面板是改版时为保旧功能补回的,用户现明确不要)。只删展示模块,后端活动/审计记录本身保留不动。
## 13. 设置页「任务队列」表格排紧凑(2026-10-21 04:58,用户附截图)
现状毛病:列宽分配失衡——任务 ID 被挤到换行两行、状态"已完成"竖排三行、时间两行,而错误列大片空白。
要求:重配列宽与换行——ID/状态/时间等短内容不换行、单行呈现;空白列不占大宽度;整表紧凑。合并进设置页后保持此排版。
+124
View File
@@ -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")
}
}
+23 -4
View File
@@ -392,11 +392,30 @@ export class Normalizer {
if (this.panelOrigin) out = out.split(this.panelOrigin).join('<panel>'); if (this.panelOrigin) out = out.split(this.panelOrigin).join('<panel>');
// Learned secrets first (longest raw first so a full token wins over its // 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. // 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); // A base64 secret's prefix truncations (the SS2022 user keys derived
for (const [raw, placeholder] of secrets) { // from a customer's 32-byte root key) are the same logical secret, so
if (raw.length >= 8) out = out.split(raw).join(placeholder); // their encodings are replaced with the same placeholder.
if (raw.length > 16) out = out.split(raw.slice(0, 16)).join(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); 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)); 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); for (const [fragment, placeholder] of this.fragments) out = out.split(fragment).join(placeholder);
+128 -99
View File
@@ -1,6 +1,6 @@
{ {
"meta": { "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." "note": "Go panel golden baseline; see README.md in this directory."
}, },
"sections": { "sections": {
@@ -49,6 +49,11 @@
"customers": [ "customers": [
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#1",
"shortId": "<secret>#2"
},
"deviceLimit": 0, "deviceLimit": 0,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -83,6 +88,11 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"deviceLimit": 2, "deviceLimit": 2,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -170,7 +180,7 @@
"body": { "body": {
"nodeClients": [ "nodeClients": [
{ {
"clientUri": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=45584502b5567121&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "clientUri": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#1", "customerId": "<cus>#1",
"id": "<ncl>#1", "id": "<ncl>#1",
@@ -179,7 +189,7 @@
"updatedAt": "<ts>" "updatedAt": "<ts>"
}, },
{ {
"clientUri": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=0adf5e3d9a002211&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "clientUri": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#2", "customerId": "<cus>#2",
"id": "<ncl>#2", "id": "<ncl>#2",
@@ -248,7 +258,7 @@
"epoch": "diff-epoch-1", "epoch": "diff-epoch-1",
"uplink": 4000 "uplink": 4000
}, },
"wsPath": "<secret>#1" "wsPath": "<secret>#5"
}, },
{ {
"artifacts": {}, "artifacts": {},
@@ -286,7 +296,7 @@
"topology": "direct", "topology": "direct",
"updatedAt": "<ts>", "updatedAt": "<ts>",
"usageCounters": null, "usageCounters": null,
"wsPath": "<secret>#2" "wsPath": "<secret>#6"
} }
] ]
}, },
@@ -555,51 +565,55 @@
"credentials": { "credentials": {
"alice": { "alice": {
"body": { "body": {
"customerId": "<cus>#1", "bindings": [
"items": [
{ {
"clientId": "<uuid>#1",
"nodeClientId": "<ncl>#1", "nodeClientId": "<ncl>#1",
"nodeId": "<nod>#2", "nodeId": "<nod>#2",
"nodeName": "N1", "nodeName": "N1",
"password": "<secret>#3",
"protocol": "vless-reality-vision", "protocol": "vless-reality-vision",
"shortId": "<secret>#4", "realityPublicKey": "<rpub>#1",
"status": "active", "status": "active",
"username": "" "username": ""
}, },
{ {
"clientId": "<uuid>#3",
"nodeClientId": "<ncl>#3", "nodeClientId": "<ncl>#3",
"nodeId": "<nod>#1", "nodeId": "<nod>#1",
"nodeName": "N2", "nodeName": "N2",
"password": "<secret>#5",
"protocol": "shadowsocks-2022-aes128", "protocol": "shadowsocks-2022-aes128",
"shortId": "<secret>#6", "realityPublicKey": null,
"status": "active", "status": "active",
"username": "" "username": ""
} }
], ],
"credentials": {
"clientId": "<uuid>#1",
"password": "<secret>#1",
"shortId": "<secret>#2"
},
"customerId": "<cus>#1",
"subscriptionToken": "<subtoken>#1" "subscriptionToken": "<subtoken>#1"
}, },
"status": 200 "status": 200
}, },
"bob": { "bob": {
"body": { "body": {
"customerId": "<cus>#2", "bindings": [
"items": [
{ {
"clientId": "<uuid>#2",
"nodeClientId": "<ncl>#2", "nodeClientId": "<ncl>#2",
"nodeId": "<nod>#2", "nodeId": "<nod>#2",
"nodeName": "N1", "nodeName": "N1",
"password": "<secret>#7",
"protocol": "vless-reality-vision", "protocol": "vless-reality-vision",
"shortId": "<secret>#8", "realityPublicKey": "<rpub>#1",
"status": "active", "status": "active",
"username": "" "username": ""
} }
], ],
"credentials": {
"clientId": "<uuid>#2",
"password": "<secret>#3",
"shortId": "<secret>#4"
},
"customerId": "<cus>#2",
"subscriptionToken": "<subtoken>#2" "subscriptionToken": "<subtoken>#2"
}, },
"status": 200 "status": 200
@@ -608,49 +622,49 @@
"subscriptionsSeed": { "subscriptionsSeed": {
"alice": { "alice": {
"auto-clash": { "auto-clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#4\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: <port>#3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#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: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#2\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: <port>#3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#1\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\n - \"N2\"\nrules:\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
}, },
"auto-generic": { "auto-generic": {
"body": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://<ssblob>#1@entry.example.com:<port>#3#N2\n", "body": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://<ssblob>#1@entry.example.com:<port>#3#N2\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
}, },
"base64": { "base64": {
"body": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://<ssblob>#1@entry.example.com:<port>#3#N2\n", "body": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://<ssblob>#1@entry.example.com:<port>#3#N2\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
}, },
"clash": { "clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#4\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: <port>#3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#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: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#2\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: <port>#3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#1\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N1\"\n - \"N2\"\nrules:\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
}, },
"clash-smart": { "clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#4\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: <port>#3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#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: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#1\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#2\"\n flow: \"xtls-rprx-vision\"\n - name: \"N2\"\n server: \"entry.example.com\"\n port: <port>#3\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#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", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
}, },
"raw": { "raw": {
"body": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://<ssblob>#1@entry.example.com:<port>#3#N2\n", "body": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\nss://<ssblob>#1@entry.example.com:<port>#3#N2\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
}, },
"singbox": { "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\": <port>#2,\n \"uuid\": \"<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\": \"<rpub>#1\",\n \"short_id\": \"<secret>#4\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": <port>#3,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#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\": <port>#2,\n \"uuid\": \"<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\": \"<rpub>#1\",\n \"short_id\": \"<secret>#2\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": <port>#3,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"xOcB7XGgo6VpaEHZfNXrlQ==:<secret>#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", "contentType": "application/json; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
}, },
"surge": { "surge": {
"body": "[General]\nloglevel = notify\n[Proxy]\nN2 = ss, entry.example.com, <port>#3, encrypt-method=2022-blake3-aes-128-gcm, password=xOcB7XGgo6VpaEHZfNXrlQ__:<secret>#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, <port>#3, encrypt-method=2022-blake3-aes-128-gcm, password=xOcB7XGgo6VpaEHZfNXrlQ__:<secret>#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", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0" "userinfo": "upload=1000; download=2000; total=0; expire=0"
@@ -658,7 +672,7 @@
}, },
"bob": { "bob": {
"auto-clash": { "auto-clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#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: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#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", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0" "userinfo": "upload=0; download=0; total=1073741824; expire=0"
@@ -670,31 +684,31 @@
"userinfo": "" "userinfo": ""
}, },
"base64": { "base64": {
"body": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n", "body": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0" "userinfo": "upload=0; download=0; total=1073741824; expire=0"
}, },
"clash": { "clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#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: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#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", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0" "userinfo": "upload=0; download=0; total=1073741824; expire=0"
}, },
"clash-smart": { "clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#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: <port>#2\n type: \"vless\"\n uuid: \"<uuid>#2\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#1\"\n short-id: \"<secret>#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", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0" "userinfo": "upload=0; download=0; total=1073741824; expire=0"
}, },
"raw": { "raw": {
"body": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n", "body": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0" "userinfo": "upload=0; download=0; total=1073741824; expire=0"
}, },
"singbox": { "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\": <port>#2,\n \"uuid\": \"<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\": \"<rpub>#1\",\n \"short_id\": \"<secret>#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\": <port>#2,\n \"uuid\": \"<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\": \"<rpub>#1\",\n \"short_id\": \"<secret>#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", "contentType": "application/json; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0" "userinfo": "upload=0; download=0; total=1073741824; expire=0"
@@ -751,6 +765,11 @@
"body": { "body": {
"customer": { "customer": {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"deviceLimit": 0, "deviceLimit": 0,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -869,7 +888,7 @@
}, },
"private_key": "${REALITY_PRIVATE:reality-<nod>#3}", "private_key": "${REALITY_PRIVATE:reality-<nod>#3}",
"short_id": [ "short_id": [
"<secret>#10" "<secret>#8"
] ]
}, },
"server_name": "www.amd.com" "server_name": "www.amd.com"
@@ -878,7 +897,7 @@
"users": [ "users": [
{ {
"name": "ng:<cus>#3", "name": "ng:<cus>#3",
"uuid": "<uuid>#4" "uuid": "<uuid>#3"
} }
] ]
} }
@@ -946,7 +965,7 @@
}, },
"private_key": "${REALITY_PRIVATE:reality-<nod>#3}", "private_key": "${REALITY_PRIVATE:reality-<nod>#3}",
"short_id": [ "short_id": [
"<secret>#10" "<secret>#8"
] ]
}, },
"server_name": "www.amd.com" "server_name": "www.amd.com"
@@ -955,7 +974,7 @@
"users": [ "users": [
{ {
"name": "ng:<cus>#3", "name": "ng:<cus>#3",
"uuid": "<uuid>#4" "uuid": "<uuid>#3"
} }
] ]
} }
@@ -994,7 +1013,7 @@
"body": { "body": {
"nodeClients": [ "nodeClients": [
{ {
"clientUri": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "clientUri": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#1", "customerId": "<cus>#1",
"id": "<ncl>#1", "id": "<ncl>#1",
@@ -1003,7 +1022,7 @@
"updatedAt": "<ts>" "updatedAt": "<ts>"
}, },
{ {
"clientUri": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1", "clientUri": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#2", "customerId": "<cus>#2",
"id": "<ncl>#2", "id": "<ncl>#2",
@@ -1021,7 +1040,7 @@
"updatedAt": "<ts>" "updatedAt": "<ts>"
}, },
{ {
"clientUri": "vless://<uuid>#4@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#10&type=tcp&headerType=none#N3", "clientUri": "vless://<uuid>#3@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#8&type=tcp&headerType=none#N3",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#3", "customerId": "<cus>#3",
"id": "<ncl>#4", "id": "<ncl>#4",
@@ -1081,7 +1100,7 @@
"epoch": "diff-epoch-1", "epoch": "diff-epoch-1",
"uplink": 4000 "uplink": 4000
}, },
"wsPath": "<secret>#1" "wsPath": "<secret>#5"
}, },
{ {
"artifacts": {}, "artifacts": {},
@@ -1119,7 +1138,7 @@
"topology": "direct", "topology": "direct",
"updatedAt": "<ts>", "updatedAt": "<ts>",
"usageCounters": null, "usageCounters": null,
"wsPath": "<secret>#2" "wsPath": "<secret>#6"
}, },
{ {
"artifacts": { "artifacts": {
@@ -1172,43 +1191,43 @@
}, },
"carolSubscriptions": { "carolSubscriptions": {
"auto-clash": { "auto-clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: <port>#4\n type: \"vless\"\n uuid: \"<uuid>#4\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#2\"\n short-id: \"<secret>#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: <port>#4\n type: \"vless\"\n uuid: \"<uuid>#3\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#2\"\n short-id: \"<secret>#8\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N3\"\nrules:\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0" "userinfo": "upload=0; download=0; total=0; expire=0"
}, },
"auto-generic": { "auto-generic": {
"body": "vless://<uuid>#4@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#10&type=tcp&headerType=none#N3\n", "body": "vless://<uuid>#3@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#8&type=tcp&headerType=none#N3\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0" "userinfo": "upload=0; download=0; total=0; expire=0"
}, },
"base64": { "base64": {
"body": "vless://<uuid>#4@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#10&type=tcp&headerType=none#N3\n", "body": "vless://<uuid>#3@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#8&type=tcp&headerType=none#N3\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0" "userinfo": "upload=0; download=0; total=0; expire=0"
}, },
"clash": { "clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: <port>#4\n type: \"vless\"\n uuid: \"<uuid>#4\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#2\"\n short-id: \"<secret>#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: <port>#4\n type: \"vless\"\n uuid: \"<uuid>#3\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#2\"\n short-id: \"<secret>#8\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"N3\"\nrules:\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0" "userinfo": "upload=0; download=0; total=0; expire=0"
}, },
"clash-smart": { "clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: <port>#4\n type: \"vless\"\n uuid: \"<uuid>#4\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#2\"\n short-id: \"<secret>#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: <port>#4\n type: \"vless\"\n uuid: \"<uuid>#3\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"<rpub>#2\"\n short-id: \"<secret>#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", "contentType": "text/yaml; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0" "userinfo": "upload=0; download=0; total=0; expire=0"
}, },
"raw": { "raw": {
"body": "vless://<uuid>#4@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#10&type=tcp&headerType=none#N3\n", "body": "vless://<uuid>#3@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#8&type=tcp&headerType=none#N3\n",
"contentType": "text/plain; charset=utf-8", "contentType": "text/plain; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0" "userinfo": "upload=0; download=0; total=0; expire=0"
}, },
"singbox": { "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\": <port>#4,\n \"uuid\": \"<uuid>#4\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"<rpub>#2\",\n \"short_id\": \"<secret>#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\": <port>#4,\n \"uuid\": \"<uuid>#3\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"<rpub>#2\",\n \"short_id\": \"<secret>#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", "contentType": "application/json; charset=utf-8",
"status": 200, "status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0" "userinfo": "upload=0; download=0; total=0; expire=0"
@@ -1286,6 +1305,11 @@
"customers": [ "customers": [
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#1",
"shortId": "<secret>#2"
},
"deviceLimit": 0, "deviceLimit": 0,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -1329,6 +1353,11 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"deviceLimit": 2, "deviceLimit": 2,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -1353,6 +1382,11 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"deviceLimit": 0, "deviceLimit": 0,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -1441,7 +1475,7 @@
"epoch": "golden-epoch-1", "epoch": "golden-epoch-1",
"uplink": 8000 "uplink": 8000
}, },
"wsPath": "<secret>#1" "wsPath": "<secret>#5"
} }
}, },
"status": 200 "status": 200
@@ -1485,8 +1519,8 @@
}, },
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}", "private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [ "short_id": [
"<secret>#4", "<secret>#2",
"<secret>#8" "<secret>#4"
] ]
}, },
"server_name": "www.amd.com" "server_name": "www.amd.com"
@@ -1515,7 +1549,7 @@
"outbounds": [ "outbounds": [
{ {
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"server": "exit.example.com", "server": "exit.example.com",
"server_port": "<port>#1", "server_port": "<port>#1",
"tag": "ng-<nod>#2-exit", "tag": "ng-<nod>#2-exit",
@@ -1573,8 +1607,8 @@
}, },
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}", "private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [ "short_id": [
"<secret>#4", "<secret>#2",
"<secret>#8" "<secret>#4"
] ]
}, },
"server_name": "www.amd.com" "server_name": "www.amd.com"
@@ -1603,7 +1637,7 @@
"outbounds": [ "outbounds": [
{ {
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"server": "exit.example.com", "server": "exit.example.com",
"server_port": "<port>#1", "server_port": "<port>#1",
"tag": "ng-<nod>#2-exit", "tag": "ng-<nod>#2-exit",
@@ -1650,7 +1684,7 @@
"listen": "<hostip>", "listen": "<hostip>",
"listen_port": "<port>#1", "listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in", "tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks" "type": "shadowsocks"
} }
@@ -1701,7 +1735,7 @@
"listen": "<hostip>", "listen": "<hostip>",
"listen_port": "<port>#1", "listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in", "tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks" "type": "shadowsocks"
} }
@@ -1740,7 +1774,7 @@
"body": { "body": {
"nodeClients": [ "nodeClients": [
{ {
"clientUri": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed", "clientUri": "vless://<uuid>#1@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#2&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#1", "customerId": "<cus>#1",
"id": "<ncl>#1", "id": "<ncl>#1",
@@ -1749,7 +1783,7 @@
"updatedAt": "<ts>" "updatedAt": "<ts>"
}, },
{ {
"clientUri": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#8&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed", "clientUri": "vless://<uuid>#2@entry.example.com:<port>#2?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#1&sid=<secret>#4&type=tcp&headerType=none&flow=xtls-rprx-vision#N1-renamed",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#2", "customerId": "<cus>#2",
"id": "<ncl>#2", "id": "<ncl>#2",
@@ -1767,7 +1801,7 @@
"updatedAt": "<ts>" "updatedAt": "<ts>"
}, },
{ {
"clientUri": "vless://<uuid>#4@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#10&type=tcp&headerType=none#N3", "clientUri": "vless://<uuid>#3@s3.example.com:<port>#4?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=<rpub>#2&sid=<secret>#8&type=tcp&headerType=none#N3",
"createdAt": "<ts>", "createdAt": "<ts>",
"customerId": "<cus>#3", "customerId": "<cus>#3",
"id": "<ncl>#4", "id": "<ncl>#4",
@@ -1834,7 +1868,7 @@
"epoch": "golden-epoch-1", "epoch": "golden-epoch-1",
"uplink": 8000 "uplink": 8000
}, },
"wsPath": "<secret>#1" "wsPath": "<secret>#5"
}, },
{ {
"artifacts": {}, "artifacts": {},
@@ -1874,7 +1908,7 @@
"topology": "direct", "topology": "direct",
"updatedAt": "<ts>", "updatedAt": "<ts>",
"usageCounters": null, "usageCounters": null,
"wsPath": "<secret>#2" "wsPath": "<secret>#6"
}, },
{ {
"artifacts": { "artifacts": {
@@ -2272,6 +2306,11 @@
"customers": [ "customers": [
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#1",
"shortId": "<secret>#2"
},
"deviceLimit": 0, "deviceLimit": 0,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -2309,6 +2348,11 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"deviceLimit": 2, "deviceLimit": 2,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -2327,6 +2371,11 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"deviceLimit": 0, "deviceLimit": 0,
"expiresAt": null, "expiresAt": null,
"group": "", "group": "",
@@ -2406,8 +2455,8 @@
}, },
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}", "private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [ "short_id": [
"<secret>#4", "<secret>#2",
"<secret>#8" "<secret>#4"
] ]
}, },
"server_name": "www.amd.com" "server_name": "www.amd.com"
@@ -2436,7 +2485,7 @@
"outbounds": [ "outbounds": [
{ {
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"server": "exit.example.com", "server": "exit.example.com",
"server_port": "<port>#1", "server_port": "<port>#1",
"tag": "ng-<nod>#2-exit", "tag": "ng-<nod>#2-exit",
@@ -2478,7 +2527,7 @@
"listen": "<hostip>", "listen": "<hostip>",
"listen_port": "<port>#1", "listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in", "tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks" "type": "shadowsocks"
} }
@@ -2529,13 +2578,13 @@
"listen": "<hostip>", "listen": "<hostip>",
"listen_port": "<port>#3", "listen_port": "<port>#3",
"method": "2022-blake3-aes-128-gcm", "method": "2022-blake3-aes-128-gcm",
"password": "<secret>#12", "password": "<secret>#11",
"tag": "ng-<nod>#1-in", "tag": "ng-<nod>#1-in",
"type": "shadowsocks", "type": "shadowsocks",
"users": [ "users": [
{ {
"name": "ng:<cus>#1", "name": "ng:<cus>#1",
"password": "<secret>#5" "password": "<secret>#12"
} }
] ]
} }
@@ -2603,7 +2652,7 @@
}, },
"private_key": "${REALITY_PRIVATE:reality-<nod>#3}", "private_key": "${REALITY_PRIVATE:reality-<nod>#3}",
"short_id": [ "short_id": [
"<secret>#10" "<secret>#8"
] ]
}, },
"server_name": "www.amd.com" "server_name": "www.amd.com"
@@ -2612,7 +2661,7 @@
"users": [ "users": [
{ {
"name": "ng:<cus>#3", "name": "ng:<cus>#3",
"uuid": "<uuid>#4" "uuid": "<uuid>#3"
} }
] ]
} }
@@ -2680,8 +2729,8 @@
}, },
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}", "private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [ "short_id": [
"<secret>#4", "<secret>#2",
"<secret>#8" "<secret>#4"
] ]
}, },
"server_name": "www.amd.com" "server_name": "www.amd.com"
@@ -2710,7 +2759,7 @@
"outbounds": [ "outbounds": [
{ {
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"server": "exit.example.com", "server": "exit.example.com",
"server_port": "<port>#1", "server_port": "<port>#1",
"tag": "ng-<nod>#2-exit", "tag": "ng-<nod>#2-exit",
@@ -2752,7 +2801,7 @@
"listen": "<hostip>", "listen": "<hostip>",
"listen_port": "<port>#1", "listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm", "method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11", "password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in", "tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks" "type": "shadowsocks"
} }
@@ -2800,11 +2849,6 @@
"nodeClients": [ "nodeClients": [
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"customerId": "<cus>#1", "customerId": "<cus>#1",
"id": "<ncl>#1", "id": "<ncl>#1",
"nodeId": "<nod>#2", "nodeId": "<nod>#2",
@@ -2813,11 +2857,6 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"customerId": "<cus>#2", "customerId": "<cus>#2",
"id": "<ncl>#2", "id": "<ncl>#2",
"nodeId": "<nod>#2", "nodeId": "<nod>#2",
@@ -2826,11 +2865,6 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#5",
"shortId": "<secret>#6"
},
"customerId": "<cus>#1", "customerId": "<cus>#1",
"id": "<ncl>#3", "id": "<ncl>#3",
"nodeId": "<nod>#1", "nodeId": "<nod>#1",
@@ -2839,11 +2873,6 @@
}, },
{ {
"createdAt": "<ts>", "createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#4",
"relayPassword": "<secret>#13",
"shortId": "<secret>#10"
},
"customerId": "<cus>#3", "customerId": "<cus>#3",
"id": "<ncl>#4", "id": "<ncl>#4",
"nodeId": "<nod>#3", "nodeId": "<nod>#3",
@@ -2861,8 +2890,8 @@
"entryReady": true, "entryReady": true,
"error": null, "error": null,
"exitCredentials": { "exitCredentials": {
"clientId": "<uuid>#5", "clientId": "<uuid>#4",
"password": "<secret>#11", "password": "<secret>#10",
"socksUser": "ng_5b8dd6b991" "socksUser": "ng_5b8dd6b991"
}, },
"exitPort": "<port>#1", "exitPort": "<port>#1",
@@ -2909,7 +2938,7 @@
"epoch": "golden-epoch-1", "epoch": "golden-epoch-1",
"uplink": 8000 "uplink": 8000
}, },
"wsPath": "<secret>#1" "wsPath": "<secret>#5"
}, },
{ {
"artifacts": {}, "artifacts": {},
@@ -2938,12 +2967,12 @@
"retiredUsageEpochs": [], "retiredUsageEpochs": [],
"serverId": "<srv>#1", "serverId": "<srv>#1",
"sortIndex": 1, "sortIndex": 1,
"ssServerKey": "<secret>#12", "ssServerKey": "<secret>#11",
"status": "active", "status": "active",
"topology": "direct", "topology": "direct",
"updatedAt": "<ts>", "updatedAt": "<ts>",
"usageCounters": null, "usageCounters": null,
"wsPath": "<secret>#2" "wsPath": "<secret>#6"
}, },
{ {
"artifacts": { "artifacts": {
+72 -63
View File
@@ -603,9 +603,40 @@ func CustomerSummary(data *Data, nowMs int64) map[string]any {
} }
} }
// credentialItems mirrors credentialItems in server.js. // realityPublicKeyOf returns the node's panel-hosted Reality public key as
func credentialItems(d *Data, customer *Customer) []map[string]any { // reported back by the agent ("" when unknown). The keypair is the server's
items := []map[string]any{} // 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 { for _, nc := range d.NodeClients {
if nc.CustomerID != customer.ID { if nc.CustomerID != customer.ID {
continue continue
@@ -618,30 +649,26 @@ func credentialItems(d *Data, customer *Customer) []map[string]any {
if node.Protocol == "socks5-auth" { if node.Protocol == "socks5-auth" {
username = NodeEmail(customer.ID) username = NodeEmail(customer.ID)
} }
creds := nc.Credentials var publicKey any
var clientID, password, shortID any if key := realityPublicKeyOf(node); key != "" {
if creds.ClientID != "" { publicKey = key
clientID = creds.ClientID
} }
if creds.RelayPassword != "" { bindings = append(bindings, map[string]any{
password = creds.RelayPassword "nodeClientId": nc.ID,
} "nodeId": node.ID,
if creds.ShortID != "" { "nodeName": node.Name,
shortID = creds.ShortID "protocol": node.Protocol,
} "status": nc.Status,
items = append(items, map[string]any{ "username": username,
"nodeClientId": nc.ID, "realityPublicKey": publicKey,
"nodeId": node.ID,
"nodeName": node.Name,
"protocol": node.Protocol,
"status": nc.Status,
"clientId": clientID,
"password": password,
"shortId": shortID,
"username": username,
}) })
} }
return items return map[string]any{
"customerId": customer.ID,
"subscriptionToken": customer.SubscriptionToken,
"credentials": creds,
"bindings": bindings,
}
} }
// CustomerBlockReason is customerBlockReason: "expired" / "traffic_limit" / // 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), Notes: CleanText(body["notes"], 1000),
CreatedAt: NowISO(), CreatedAt: NowISO(),
UpdatedAt: NowISO(), UpdatedAt: NowISO(),
Credentials: func() *Credentials { c := NewCustomerCredentials(); return &c }(),
} }
// Arm the monthly-reset schedule: it starts with the next // Arm the monthly-reset schedule: it starts with the next
// reset instant, it never fires retroactively at creation. // 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 { if customer == nil {
return true, httpErr(404, "客户不存在") return true, httpErr(404, "客户不存在")
} }
SendJSON(w, 200, map[string]any{ SendJSON(w, 200, customerCredentialsView(data, customer), nil)
"customerId": customer.ID,
"subscriptionToken": customer.SubscriptionToken,
"items": credentialItems(data, customer),
}, nil)
return true, nil return true, nil
} }
if params, ok := RoutePattern("/api/customers/:id/credentials/:ncid/regenerate", pathname); ok && r.Method == http.MethodPost { if params, ok := RoutePattern("/api/customers/:id/credentials/regenerate", pathname); ok && r.Method == http.MethodPost {
var responseItem map[string]any var view map[string]any
_, err := a.store.Transaction(func(d *Data) (any, error) { _, err := a.store.Transaction(func(d *Data) (any, error) {
customer := findCustomer(d, params["id"]) customer := findCustomer(d, params["id"])
if customer == nil { if customer == nil {
return nil, httpErr(404, "客户不存在") 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 { for _, nc := range d.NodeClients {
if nc.ID == params["ncid"] && nc.CustomerID == customer.ID { if nc.CustomerID != customer.ID || nc.Status != "active" {
binding = nc continue
break
} }
} node := findNode(d, nc.NodeID)
if binding == nil { if node == nil || node.Status == "draft" || node.Status == "removing" || rebuilt[node.ID] {
return nil, httpErr(404, "凭据不存在") continue
} }
node := findNode(d, binding.NodeID) rebuilt[node.ID] = true
if node == nil {
return nil, httpErr(404, "节点不存在")
}
binding.Credentials = NewNodeClientCredentials(node.Protocol)
binding.UpdatedAt = NowISO()
if node.Status != "draft" && node.Status != "removing" {
if err := RebuildNode(d, node, false); err != nil { if err := RebuildNode(d, node, false); err != nil {
return nil, err return nil, err
} }
} }
Audit(d, actor, "regenerate_credentials", customer.ID, map[string]any{"nodeId": node.ID, "node": node.Name}) Audit(d, actor, "regenerate_credentials", customer.ID, map[string]any{"nodesRebuilt": len(rebuilt)})
for _, entry := range credentialItems(d, customer) { view = customerCredentialsView(d, customer)
if entry["nodeClientId"] == binding.ID {
responseItem = entry
break
}
}
return nil, nil return nil, nil
}) })
if err != nil { if err != nil {
return true, err return true, err
} }
SendJSON(w, 200, map[string]any{"item": responseItem}, nil) SendJSON(w, 200, view, nil)
return true, nil return true, nil
} }
if params, ok := RoutePattern("/api/customers/:id/external-sources/:sid/refresh", pathname); ok && r.Method == http.MethodPost { 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 oldServerID := item.ServerID
oldExitServerID := strVal(item.ExitServerID) oldExitServerID := strVal(item.ExitServerID)
oldForward := forwardNode(item) oldForward := forwardNode(item)
oldProtocol := item.Protocol
next, err := NormalizeNode(body, item, d) next, err := NormalizeNode(body, item, d)
if err != nil { if err != nil {
return nil, err return nil, err
} }
*item = *next *item = *next
EnsureNodeSecrets(item) 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 { if item.ServerID != oldServerID {
QueueJob(d, oldServerID, item.ID, "delete_resource", &JobPayload{ResourceID: item.ResourceID, Kind: "node"}) QueueJob(d, oldServerID, item.ID, "delete_resource", &JobPayload{ResourceID: item.ResourceID, Kind: "node"})
item.EntryReady = false item.EntryReady = false
+40 -15
View File
@@ -239,6 +239,16 @@ func payloadWithResource(resource map[string]any, kind string, keyMaterial map[s
return payload 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) { func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any, error) {
server := orchFindServer(d, node.ServerID) server := orchFindServer(d, node.ServerID)
if server == nil { if server == nil {
@@ -262,11 +272,26 @@ func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any
if clients == nil { if clients == nil {
clients = activeNodeClients(d, node) 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{ return BuildNodeResource(BuildNodeResourceParams{
ResourceID: node.ResourceID, ResourceID: node.ResourceID,
TagPrefix: tagForNode(node), TagPrefix: tagForNode(node),
Node: node, Node: node,
NodeClients: clients, NodeClients: resolved,
Server: server, Server: server,
BindAddress: bindAddress, BindAddress: bindAddress,
ExitServer: exitServer, ExitServer: exitServer,
@@ -977,14 +1002,14 @@ func assignCustomerData(d *Data, nodeID, customerID, actor string) (*NodeClient,
nc.UpdatedAt = NowISO() nc.UpdatedAt = NowISO()
} else { } else {
now := NowISO() now := NowISO()
ensureCustomerCredentials(d, customerID)
nc = &NodeClient{ nc = &NodeClient{
ID: NewID("ncl"), ID: NewID("ncl"),
NodeID: nodeID, NodeID: nodeID,
CustomerID: customerID, CustomerID: customerID,
Credentials: NewNodeClientCredentials(node.Protocol), Status: "active",
Status: "active", CreatedAt: now,
CreatedAt: now, UpdatedAt: now,
UpdatedAt: now,
} }
d.NodeClients = append(d.NodeClients, nc) d.NodeClients = append(d.NodeClients, nc)
} }
@@ -1077,14 +1102,14 @@ func AlignCustomerNodes(d *Data, customer *Customer, desiredIds []string, actor
status = "active" status = "active"
} }
now := NowISO() now := NowISO()
ensureCustomerCredentials(d, customer.ID)
d.NodeClients = append(d.NodeClients, &NodeClient{ d.NodeClients = append(d.NodeClients, &NodeClient{
ID: NewID("ncl"), ID: NewID("ncl"),
NodeID: nodeID, NodeID: nodeID,
CustomerID: customer.ID, CustomerID: customer.ID,
Credentials: NewNodeClientCredentials(node.Protocol), Status: status,
Status: status, CreatedAt: now,
CreatedAt: now, UpdatedAt: now,
UpdatedAt: now,
}) })
Audit(d, actor, "assign_customer", node.ID, map[string]any{"customerId": customer.ID, "customer": customer.Name}) Audit(d, actor, "assign_customer", node.ID, map[string]any{"customerId": customer.ID, "customer": customer.Name})
if status == "active" { if status == "active" {
+45 -15
View File
@@ -196,18 +196,47 @@ func (q xbQuery) String() string {
func NodeEmail(customerID string) string { return "ng:" + customerID } func NodeEmail(customerID string) string { return "ng:" + customerID }
func NewNodeClientCredentials(protocol string) Credentials { // NewCustomerCredentials mints the single credential set a customer uses on
keyBytes := 24 // every node (round-2 item 2, 3x-ui model). The relay password is always a
if cipher, ok := ShadowsocksMethods[protocol]; ok { // base64 32-byte root key: SS2022 user keys are derived from it per method
keyBytes = cipher.KeyBytes // (see EffectiveRelayPassword), so one set serves aes128 and aes256 nodes
} // alike; every other protocol uses it verbatim.
func NewCustomerCredentials() Credentials {
return Credentials{ return Credentials{
ClientID: NewUUID(), ClientID: NewUUID(),
RelayPassword: b64(keyBytes), RelayPassword: b64(32),
ShortID: shortId(), 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) { func EnsureNodeSecrets(node *Node) {
if node.WSPath == nil || *node.WSPath == "" { if node.WSPath == nil || *node.WSPath == "" {
node.WSPath = StrPtr("/" + hex.EncodeToString(xbRandBytes(9))) 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)) users := make([]any, 0, len(nodeClients))
shortIDs := make([]any, 0, len(nodeClients)) shortIDs := make([]any, 0, len(nodeClients))
for _, nc := range 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" { if protocol == "vless-reality-vision" {
user["flow"] = "xtls-rprx-vision" user["flow"] = "xtls-rprx-vision"
} }
users = append(users, user) users = append(users, user)
shortIDs = append(shortIDs, nc.Credentials.ShortID) shortIDs = append(shortIDs, creds.ShortID)
} }
destPort := node.Reality.DestPort destPort := node.Reality.DestPort
if destPort == 0 { if destPort == 0 {
@@ -383,7 +413,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
} }
users := make([]any, 0, len(nodeClients)) users := make([]any, 0, len(nodeClients))
for _, nc := range 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) wsPath := xbDeref(node.WSPath)
if wsPath == "" { if wsPath == "" {
@@ -406,7 +436,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
if cipher, ok := ShadowsocksMethods[protocol]; ok { if cipher, ok := ShadowsocksMethods[protocol]; ok {
users := make([]any, 0, len(nodeClients)) users := make([]any, 0, len(nodeClients))
for _, nc := range 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{ return map[string]any{
"type": "shadowsocks", "tag": tag, "listen": listen, "listen_port": port, "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" { if protocol == "socks5-auth" {
users := make([]any, 0, len(nodeClients)) users := make([]any, 0, len(nodeClients))
for _, nc := range 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 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)) users := make([]any, 0, len(nodeClients))
for _, nc := range 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{ return map[string]any{
"type": "hysteria2", "tag": tag, "listen": listen, "listen_port": port, "users": users, "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)) users := make([]any, 0, len(nodeClients))
for _, nc := range 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{ return map[string]any{
"type": "anytls", "tag": tag, "listen": listen, "listen_port": port, "users": users, "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 return fmt.Sprintf("vless://%s@%s:%d?%s#%s", p.ClientID, host, p.RelayPort, q.String(), label), true
} }
if cipher, ok := ShadowsocksMethods[protocol]; ok { if cipher, ok := ShadowsocksMethods[protocol]; ok {
password := p.RelayPassword password := EffectiveRelayPassword(protocol, p.RelayPassword)
if strings.HasPrefix(cipher.Method, "2022-blake3-") && p.SSServerKey != "" { 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)) user := base64.RawURLEncoding.EncodeToString([]byte(cipher.Method + ":" + password))
return fmt.Sprintf("ss://%s@%s:%d#%s", user, host, p.RelayPort, label), true return fmt.Sprintf("ss://%s@%s:%d#%s", user, host, p.RelayPort, label), true
+3 -3
View File
@@ -83,8 +83,8 @@ func TestGoldenResources(t *testing.T) {
ExitCredentials: &ExitCredentials{ClientID: "11111111-2222-3333-4444-0000000000ff", Password: exitPassword, SocksUser: "ng_0123456789"}, ExitCredentials: &ExitCredentials{ClientID: "11111111-2222-3333-4444-0000000000ff", Password: exitPassword, SocksUser: "ng_0123456789"},
} }
rfClients := []*NodeClient{ 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_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_2", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000002", RelayPassword: "unused2", ShortID: "abcd0002ef01"}},
} }
entryServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.10"} entryServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.10"}
exitServer := &Server{ID: "srv_exit", PublicAddress: "exit.example.com"} exitServer := &Server{ID: "srv_exit", PublicAddress: "exit.example.com"}
@@ -102,7 +102,7 @@ func TestGoldenResources(t *testing.T) {
SSServerKey: StrPtr("CwwNDg8QERITFBUWFxgZGg=="), SSServerKey: StrPtr("CwwNDg8QERITFBUWFxgZGg=="),
} }
ssClients := []*NodeClient{ 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"} ssServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.11"}
+61
View File
@@ -6,6 +6,7 @@ package main
// the same serialization here. // the same serialization here.
import ( import (
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -133,10 +134,70 @@ func (s *Store) Init() error {
if err := validateData(&data); err != nil { if err := validateData(&data); err != nil {
return err 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 s.Data = &data
return nil 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). // Snapshot returns a deep copy of the current data (JS structuredClone).
func (s *Store) Snapshot() *Data { func (s *Store) Snapshot() *Data {
s.mu.Lock() s.mu.Lock()
+7 -3
View File
@@ -108,13 +108,17 @@ func NodeClientURI(d *Data, nc *NodeClient) string {
if networkMode == "" { if networkMode == "" {
networkMode = "ipv4" networkMode = "ipv4"
} }
creds := Credentials{}
if customer := findCustomer(d, nc.CustomerID); customer != nil && customer.Credentials != nil {
creds = *customer.Credentials
}
uri, ok := BuildClientURI(BuildClientURIParams{ uri, ok := BuildClientURI(BuildClientURIParams{
Protocol: node.Protocol, Protocol: node.Protocol,
RelayServer: server, RelayServer: server,
RelayPort: node.Port, RelayPort: node.Port,
ClientID: nc.Credentials.ClientID, ClientID: creds.ClientID,
RelayPassword: nc.Credentials.RelayPassword, RelayPassword: creds.RelayPassword,
ShortID: nc.Credentials.ShortID, ShortID: creds.ShortID,
WSPath: xbDeref(node.WSPath), WSPath: xbDeref(node.WSPath),
SocksUser: NodeEmail(nc.CustomerID), SocksUser: NodeEmail(nc.CustomerID),
Reality: node.Reality, Reality: node.Reality,
+12 -13
View File
@@ -36,26 +36,25 @@ func goldenSubData() *Data {
Reality: &RealityConfig{KeyID: "reality-nod_rv", ServerName: "www.apple.com", DestPort: 443}, Reality: &RealityConfig{KeyID: "reality-nod_rv", ServerName: "www.apple.com", DestPort: 443},
Artifacts: art("FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu")}, Artifacts: art("FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu")},
} }
nc := func(nodeID, clientID, relayPassword, shortID string) *NodeClient { nc := func(nodeID string) *NodeClient {
return &NodeClient{ return &NodeClient{NodeID: nodeID, CustomerID: "cus_1", Status: "active"}
NodeID: nodeID, CustomerID: "cus_1", Status: "active",
Credentials: Credentials{ClientID: clientID, RelayPassword: relayPassword, ShortID: shortID},
}
} }
clients := []*NodeClient{ clients := []*NodeClient{
nc("nod_rf", "11111111-2222-3333-4444-000000000001", "unused", "abcd0001ef01"), nc("nod_rf"), nc("nod_ss"), nc("nod_hy"), nc("nod_any"),
nc("nod_ss", "11111111-2222-3333-4444-000000000003", "FRYXGBkaGxwdHh8gISIjJA==", "abcd0003ef01"), nc("nod_ws"), nc("nod_sk"), nc("nod_rv"),
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"),
} }
return &Data{ return &Data{
Servers: []*Server{entryServer, v6Server}, Servers: []*Server{entryServer, v6Server},
Nodes: nodes, Nodes: nodes,
NodeClients: clients, 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",
}}},
} }
} }
+32 -32
View File
@@ -2,7 +2,7 @@
"entries": [ "entries": [
{ {
"nodeId": "nod_rf", "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", "nodeId": "nod_ss",
@@ -10,56 +10,56 @@
}, },
{ {
"nodeId": "nod_hy", "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", "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", "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", "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", "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": { "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": { "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" "contentType": "text/yaml; charset=utf-8"
}, },
"auto-plain-ua": { "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" "contentType": "text/plain; charset=utf-8"
} }
} }
+19 -11
View File
@@ -560,9 +560,13 @@ type Customer struct {
ServerUsage map[string]*UsageSlot `json:"serverUsage,omitempty"` ServerUsage map[string]*UsageSlot `json:"serverUsage,omitempty"`
LastUsageAt *string `json:"lastUsageAt,omitempty"` LastUsageAt *string `json:"lastUsageAt,omitempty"`
LastOnlineAt *string `json:"lastOnlineAt,omitempty"` LastOnlineAt *string `json:"lastOnlineAt,omitempty"`
ExternalSources []ExtSource `json:"externalSources,omitempty"` // The customer's single credential set, shared by all of their node
ManualLinks []ManualLink `json:"manualLinks,omitempty"` // bindings (round-2 item 2). Populated by the store-load migration for
Extra extraFields `json:"-"` // 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 // ExtSource is one external subscription source on a customer
@@ -781,14 +785,18 @@ func (c Credentials) MarshalJSON() ([]byte, error) {
} }
type NodeClient struct { type NodeClient struct {
ID string `json:"id"` ID string `json:"id"`
NodeID string `json:"nodeId"` NodeID string `json:"nodeId"`
CustomerID string `json:"customerId"` CustomerID string `json:"customerId"`
Credentials Credentials `json:"credentials"` // Deprecated: credentials live on the Customer (round-2 item 2). This
Status string `json:"status"` // field is only a migration input — the store-load migration converges
CreatedAt string `json:"createdAt"` // it into Customer.Credentials and clears it, so it disappears from
UpdatedAt string `json:"updatedAt"` // disk on the next save. Builders receive resolved copies instead.
Extra extraFields `json:"-"` 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 { func (n *NodeClient) UnmarshalJSON(raw []byte) error {
+4 -3
View File
@@ -53,8 +53,8 @@ func TestAggregateEntriesOrderAndDedup(t *testing.T) {
}, },
Servers: []*Server{{ID: "srv_1", Name: "S", PublicAddress: "s.example.com"}}, Servers: []*Server{{ID: "srv_1", Name: "S", PublicAddress: "s.example.com"}},
NodeClients: []*NodeClient{ NodeClients: []*NodeClient{
{ID: "ncl_b", NodeID: "nod_b", CustomerID: "cus_1", Status: "active", Credentials: Credentials{RelayPassword: "pw-b"}}, {ID: "ncl_b", NodeID: "nod_b", CustomerID: "cus_1", Status: "active"},
{ID: "ncl_a", NodeID: "nod_a", CustomerID: "cus_1", Status: "active", Credentials: Credentials{RelayPassword: "pw-a"}}, {ID: "ncl_a", NodeID: "nod_a", CustomerID: "cus_1", Status: "active"},
}, },
SubSourceCache: map[string]*SubSourceCacheEntry{ SubSourceCache: map[string]*SubSourceCacheEntry{
"src_1": {Items: []SubCacheItem{{URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Dup", Name: "Dup"}}}, "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() okAt := NowISO()
d.SubSourceCache["src_1"].OkAt = &okAt d.SubSourceCache["src_1"].OkAt = &okAt
customer := &Customer{ customer := &Customer{
ID: "cus_1", ID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "pw-a", ShortID: "abcd0001ef01"},
ManualLinks: []ManualLink{ ManualLinks: []ManualLink{
{ID: "lnk_1", URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Whatever", Remark: "手动一号", Enabled: true}, {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}}, ExternalSources: []ExtSource{{ID: "src_1", URL: "https://example.com/sub", Remark: "机场", Enabled: true}},
} }
d.Customers = []*Customer{customer}
entries := AggregateEntries(d, customer) entries := AggregateEntries(d, customer)
names := []string{} names := []string{}
for _, entry := range entries { for _, entry := range entries {
+13 -11
View File
@@ -636,19 +636,22 @@ function renderCredentialsPane(customer) {
if (!customer) { pane.innerHTML = '<div class="empty"><b>创建客户后生成凭据</b></div>'; return; } if (!customer) { pane.innerHTML = '<div class="empty"><b>创建客户后生成凭据</b></div>'; return; }
const data = customerFormState.credentials; const data = customerFormState.credentials;
if (!data) { pane.innerHTML = '<div class="empty"><b>加载中…</b></div>'; return; } if (!data) { pane.innerHTML = '<div class="empty"><b>加载中…</b></div>'; return; }
const groups = data.items.map((entry) => `<div class="cred-group"> const creds = data.credentials || {};
<div class="g-head">${esc(entry.nodeName)}<span class="badge ${entry.status === 'active' ? 'green' : 'gray'}">${esc(statusText[entry.status] || entry.status)}</span><span class="mut" style="font-weight:400;font-size:12px">${esc(entry.protocol)}</span> const bindings = (data.bindings || []).map((entry) => `<div class="cred-group">
<button type="button" class="icon-btn" style="margin-left:auto" data-tip="重新生成凭据" data-action="regen-credential" data-ncid="${esc(entry.nodeClientId)}" data-node="${esc(entry.nodeName)}">${ICONS.reset}</button></div> <div class="g-head">${esc(entry.nodeName)}<span class="badge ${entry.status === 'active' ? 'green' : 'gray'}">${esc(statusText[entry.status] || entry.status)}</span><span class="mut" style="font-weight:400;font-size:12px">${esc(entry.protocol)}</span></div>
${credRowHTML('UUID', entry.clientId)}
${credRowHTML('密码', entry.password)}
${entry.shortId ? credRowHTML('Reality Short ID', entry.shortId) : ''}
${entry.username ? credRowHTML('用户名', entry.username) : ''} ${entry.username ? credRowHTML('用户名', entry.username) : ''}
${entry.realityPublicKey ? credRowHTML('Reality 公钥', entry.realityPublicKey) : ''}
</div>`).join(''); </div>`).join('');
pane.innerHTML = `<div class="cred-group"><div class="g-head">订阅</div> pane.innerHTML = `<div class="cred-group"><div class="g-head">订阅</div>
<div class="cred-row"><span class="k">订阅令牌</span><span class="v">${esc(data.subscriptionToken || '—')}</span> <div class="cred-row"><span class="k">订阅令牌</span><span class="v">${esc(data.subscriptionToken || '—')}</span>
<button type="button" class="icon-btn" data-tip="复制" data-action="copy-text" data-value="${esc(data.subscriptionToken || '')}">${ICONS.copy}</button> <button type="button" class="icon-btn" data-tip="复制" data-action="copy-text" data-value="${esc(data.subscriptionToken || '')}">${ICONS.copy}</button>
<button type="button" class="icon-btn" data-tip="重置订阅令牌" data-action="rotate-subscription" data-id="${esc(customer.id)}">${ICONS.reset}</button></div></div> <button type="button" class="icon-btn" data-tip="重置订阅令牌" data-action="rotate-subscription" data-id="${esc(customer.id)}">${ICONS.reset}</button></div></div>
${groups || '<div class="empty"><b>尚未关联节点</b></div>'}`; <div class="cred-group"><div class="g-head">凭据(全部节点共用)<button type="button" class="icon-btn" style="margin-left:auto" data-tip="重新生成凭据" data-action="regen-credentials" data-id="${esc(customer.id)}">${ICONS.reset}</button></div>
${credRowHTML('UUID', creds.clientId)}
${credRowHTML('密码', creds.password)}
${credRowHTML('Reality Short ID', creds.shortId)}
</div>
${bindings || '<div class="empty"><b>尚未关联节点</b></div>'}`;
} }
function linkStatusLine(customer, source) { 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(() => {}); api(`/api/customers/${itemId}/credentials`).then((data) => { customerFormState.credentials = data; renderCredentialsPane(result.customer); }).catch(() => {});
} else if (modalOpen()) subscriptionModal(result.customer); } else if (modalOpen()) subscriptionModal(result.customer);
} }
else if (action === 'regen-credential') { else if (action === 'regen-credentials') {
const customerId = $('#customer-form').dataset.customerId; const customerId = $('#customer-form').dataset.customerId;
if (!await confirmModal(`重新生成「${button.dataset.node}」的凭据?该客户在此节点的现有连接将立即失效,需要重新导入订阅。`, '重新生成')) return; if (!await confirmModal('重新生成该客户的凭据?全部节点同时更换,现有连接将立即失效,需要重新导入订阅。', '重新生成')) return;
await api(`/api/customers/${customerId}/credentials/${button.dataset.ncid}/regenerate`, { method: 'POST', body: '{}' }); const data = await api(`/api/customers/${customerId}/credentials/regenerate`, { method: 'POST', body: '{}' });
const data = await api(`/api/customers/${customerId}/credentials`);
customerFormState.credentials = data; customerFormState.credentials = data;
renderCredentialsPane(customerById(customerId)); renderCredentialsPane(customerById(customerId));
toast('凭据已重新生成'); toast('凭据已重新生成');