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
+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>');
// Learned secrets first (longest raw first so a full token wins over its
// own prefixes), including the 16-char prefix shown on the info page.
const secrets = [...this.secrets.entries()].sort((a, b) => b[0].length - a[0].length);
for (const [raw, placeholder] of secrets) {
if (raw.length >= 8) out = out.split(raw).join(placeholder);
if (raw.length > 16) out = out.split(raw.slice(0, 16)).join(placeholder);
// A base64 secret's prefix truncations (the SS2022 user keys derived
// from a customer's 32-byte root key) are the same logical secret, so
// their encodings are replaced with the same placeholder.
const replacements = [];
for (const [raw, placeholder] of this.secrets) {
if (raw.length >= 8) replacements.push([raw, placeholder]);
if (raw.length > 16) replacements.push([raw.slice(0, 16), placeholder]);
if (/^[A-Za-z0-9+/]+={0,2}$/.test(raw) && raw.length % 4 === 0) {
const bytes = Buffer.from(raw, 'base64');
if (bytes.toString('base64') === raw) {
for (const n of [16, 24]) {
if (n < bytes.length) replacements.push([bytes.subarray(0, n).toString('base64'), placeholder]);
}
}
}
}
// The Surge formatter mangles '=' to '_' inside passwords, so the
// mangled variant of every replacement is the same secret too.
for (const [raw, placeholder] of [...replacements]) {
const mangled = raw.replace(/=/g, '_');
if (mangled !== raw) replacements.push([mangled, placeholder]);
}
replacements.sort((a, b) => b[0].length - a[0].length);
for (const [raw, placeholder] of replacements) out = out.split(raw).join(placeholder);
for (const [raw, placeholder] of this.hashes) out = out.split(raw).join(placeholder);
out = out.replace(PREFIXED_ID_RE, (whole, prefix) => this.#idPlaceholder(prefix, whole));
for (const [fragment, placeholder] of this.fragments) out = out.split(fragment).join(placeholder);
+128 -99
View File
@@ -1,6 +1,6 @@
{
"meta": {
"capturedAt": "2026-10-20T21:02:25.354Z",
"capturedAt": "2026-10-20T21:20:39.194Z",
"note": "Go panel golden baseline; see README.md in this directory."
},
"sections": {
@@ -49,6 +49,11 @@
"customers": [
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#1",
"shortId": "<secret>#2"
},
"deviceLimit": 0,
"expiresAt": null,
"group": "",
@@ -83,6 +88,11 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"deviceLimit": 2,
"expiresAt": null,
"group": "",
@@ -170,7 +180,7 @@
"body": {
"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>",
"customerId": "<cus>#1",
"id": "<ncl>#1",
@@ -179,7 +189,7 @@
"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>",
"customerId": "<cus>#2",
"id": "<ncl>#2",
@@ -248,7 +258,7 @@
"epoch": "diff-epoch-1",
"uplink": 4000
},
"wsPath": "<secret>#1"
"wsPath": "<secret>#5"
},
{
"artifacts": {},
@@ -286,7 +296,7 @@
"topology": "direct",
"updatedAt": "<ts>",
"usageCounters": null,
"wsPath": "<secret>#2"
"wsPath": "<secret>#6"
}
]
},
@@ -555,51 +565,55 @@
"credentials": {
"alice": {
"body": {
"customerId": "<cus>#1",
"items": [
"bindings": [
{
"clientId": "<uuid>#1",
"nodeClientId": "<ncl>#1",
"nodeId": "<nod>#2",
"nodeName": "N1",
"password": "<secret>#3",
"protocol": "vless-reality-vision",
"shortId": "<secret>#4",
"realityPublicKey": "<rpub>#1",
"status": "active",
"username": ""
},
{
"clientId": "<uuid>#3",
"nodeClientId": "<ncl>#3",
"nodeId": "<nod>#1",
"nodeName": "N2",
"password": "<secret>#5",
"protocol": "shadowsocks-2022-aes128",
"shortId": "<secret>#6",
"realityPublicKey": null,
"status": "active",
"username": ""
}
],
"credentials": {
"clientId": "<uuid>#1",
"password": "<secret>#1",
"shortId": "<secret>#2"
},
"customerId": "<cus>#1",
"subscriptionToken": "<subtoken>#1"
},
"status": 200
},
"bob": {
"body": {
"customerId": "<cus>#2",
"items": [
"bindings": [
{
"clientId": "<uuid>#2",
"nodeClientId": "<ncl>#2",
"nodeId": "<nod>#2",
"nodeName": "N1",
"password": "<secret>#7",
"protocol": "vless-reality-vision",
"shortId": "<secret>#8",
"realityPublicKey": "<rpub>#1",
"status": "active",
"username": ""
}
],
"credentials": {
"clientId": "<uuid>#2",
"password": "<secret>#3",
"shortId": "<secret>#4"
},
"customerId": "<cus>#2",
"subscriptionToken": "<subtoken>#2"
},
"status": 200
@@ -608,49 +622,49 @@
"subscriptionsSeed": {
"alice": {
"auto-clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
},
"clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
},
"clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
},
"singbox": {
"body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\",\n \"node-2\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": <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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=1000; download=2000; total=0; expire=0"
@@ -658,7 +672,7 @@
},
"bob": {
"auto-clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0"
@@ -670,31 +684,31 @@
"userinfo": ""
},
"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",
"status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0"
},
"clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0"
},
"clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N1\"\n server: \"entry.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0"
},
"singbox": {
"body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": <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",
"status": 200,
"userinfo": "upload=0; download=0; total=1073741824; expire=0"
@@ -751,6 +765,11 @@
"body": {
"customer": {
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"deviceLimit": 0,
"expiresAt": null,
"group": "",
@@ -869,7 +888,7 @@
},
"private_key": "${REALITY_PRIVATE:reality-<nod>#3}",
"short_id": [
"<secret>#10"
"<secret>#8"
]
},
"server_name": "www.amd.com"
@@ -878,7 +897,7 @@
"users": [
{
"name": "ng:<cus>#3",
"uuid": "<uuid>#4"
"uuid": "<uuid>#3"
}
]
}
@@ -946,7 +965,7 @@
},
"private_key": "${REALITY_PRIVATE:reality-<nod>#3}",
"short_id": [
"<secret>#10"
"<secret>#8"
]
},
"server_name": "www.amd.com"
@@ -955,7 +974,7 @@
"users": [
{
"name": "ng:<cus>#3",
"uuid": "<uuid>#4"
"uuid": "<uuid>#3"
}
]
}
@@ -994,7 +1013,7 @@
"body": {
"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>",
"customerId": "<cus>#1",
"id": "<ncl>#1",
@@ -1003,7 +1022,7 @@
"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>",
"customerId": "<cus>#2",
"id": "<ncl>#2",
@@ -1021,7 +1040,7 @@
"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>",
"customerId": "<cus>#3",
"id": "<ncl>#4",
@@ -1081,7 +1100,7 @@
"epoch": "diff-epoch-1",
"uplink": 4000
},
"wsPath": "<secret>#1"
"wsPath": "<secret>#5"
},
{
"artifacts": {},
@@ -1119,7 +1138,7 @@
"topology": "direct",
"updatedAt": "<ts>",
"usageCounters": null,
"wsPath": "<secret>#2"
"wsPath": "<secret>#6"
},
{
"artifacts": {
@@ -1172,43 +1191,43 @@
},
"carolSubscriptions": {
"auto-clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0"
},
"clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0"
},
"clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"N3\"\n server: \"s3.example.com\"\n port: <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",
"status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0"
},
"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",
"status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0"
},
"singbox": {
"body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"s3.example.com\",\n \"server_port\": <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",
"status": 200,
"userinfo": "upload=0; download=0; total=0; expire=0"
@@ -1286,6 +1305,11 @@
"customers": [
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#1",
"shortId": "<secret>#2"
},
"deviceLimit": 0,
"expiresAt": null,
"group": "",
@@ -1329,6 +1353,11 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"deviceLimit": 2,
"expiresAt": null,
"group": "",
@@ -1353,6 +1382,11 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"deviceLimit": 0,
"expiresAt": null,
"group": "",
@@ -1441,7 +1475,7 @@
"epoch": "golden-epoch-1",
"uplink": 8000
},
"wsPath": "<secret>#1"
"wsPath": "<secret>#5"
}
},
"status": 200
@@ -1485,8 +1519,8 @@
},
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [
"<secret>#4",
"<secret>#8"
"<secret>#2",
"<secret>#4"
]
},
"server_name": "www.amd.com"
@@ -1515,7 +1549,7 @@
"outbounds": [
{
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"server": "exit.example.com",
"server_port": "<port>#1",
"tag": "ng-<nod>#2-exit",
@@ -1573,8 +1607,8 @@
},
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [
"<secret>#4",
"<secret>#8"
"<secret>#2",
"<secret>#4"
]
},
"server_name": "www.amd.com"
@@ -1603,7 +1637,7 @@
"outbounds": [
{
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"server": "exit.example.com",
"server_port": "<port>#1",
"tag": "ng-<nod>#2-exit",
@@ -1650,7 +1684,7 @@
"listen": "<hostip>",
"listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks"
}
@@ -1701,7 +1735,7 @@
"listen": "<hostip>",
"listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks"
}
@@ -1740,7 +1774,7 @@
"body": {
"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>",
"customerId": "<cus>#1",
"id": "<ncl>#1",
@@ -1749,7 +1783,7 @@
"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>",
"customerId": "<cus>#2",
"id": "<ncl>#2",
@@ -1767,7 +1801,7 @@
"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>",
"customerId": "<cus>#3",
"id": "<ncl>#4",
@@ -1834,7 +1868,7 @@
"epoch": "golden-epoch-1",
"uplink": 8000
},
"wsPath": "<secret>#1"
"wsPath": "<secret>#5"
},
{
"artifacts": {},
@@ -1874,7 +1908,7 @@
"topology": "direct",
"updatedAt": "<ts>",
"usageCounters": null,
"wsPath": "<secret>#2"
"wsPath": "<secret>#6"
},
{
"artifacts": {
@@ -2272,6 +2306,11 @@
"customers": [
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#1",
"shortId": "<secret>#2"
},
"deviceLimit": 0,
"expiresAt": null,
"group": "",
@@ -2309,6 +2348,11 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"deviceLimit": 2,
"expiresAt": null,
"group": "",
@@ -2327,6 +2371,11 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"deviceLimit": 0,
"expiresAt": null,
"group": "",
@@ -2406,8 +2455,8 @@
},
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [
"<secret>#4",
"<secret>#8"
"<secret>#2",
"<secret>#4"
]
},
"server_name": "www.amd.com"
@@ -2436,7 +2485,7 @@
"outbounds": [
{
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"server": "exit.example.com",
"server_port": "<port>#1",
"tag": "ng-<nod>#2-exit",
@@ -2478,7 +2527,7 @@
"listen": "<hostip>",
"listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks"
}
@@ -2529,13 +2578,13 @@
"listen": "<hostip>",
"listen_port": "<port>#3",
"method": "2022-blake3-aes-128-gcm",
"password": "<secret>#12",
"password": "<secret>#11",
"tag": "ng-<nod>#1-in",
"type": "shadowsocks",
"users": [
{
"name": "ng:<cus>#1",
"password": "<secret>#5"
"password": "<secret>#12"
}
]
}
@@ -2603,7 +2652,7 @@
},
"private_key": "${REALITY_PRIVATE:reality-<nod>#3}",
"short_id": [
"<secret>#10"
"<secret>#8"
]
},
"server_name": "www.amd.com"
@@ -2612,7 +2661,7 @@
"users": [
{
"name": "ng:<cus>#3",
"uuid": "<uuid>#4"
"uuid": "<uuid>#3"
}
]
}
@@ -2680,8 +2729,8 @@
},
"private_key": "${REALITY_PRIVATE:reality-<nod>#2}",
"short_id": [
"<secret>#4",
"<secret>#8"
"<secret>#2",
"<secret>#4"
]
},
"server_name": "www.amd.com"
@@ -2710,7 +2759,7 @@
"outbounds": [
{
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"server": "exit.example.com",
"server_port": "<port>#1",
"tag": "ng-<nod>#2-exit",
@@ -2752,7 +2801,7 @@
"listen": "<hostip>",
"listen_port": "<port>#1",
"method": "2022-blake3-aes-256-gcm",
"password": "<secret>#11",
"password": "<secret>#10",
"tag": "ng-<nod>#2-exit-in",
"type": "shadowsocks"
}
@@ -2800,11 +2849,6 @@
"nodeClients": [
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#1",
"relayPassword": "<secret>#3",
"shortId": "<secret>#4"
},
"customerId": "<cus>#1",
"id": "<ncl>#1",
"nodeId": "<nod>#2",
@@ -2813,11 +2857,6 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#2",
"relayPassword": "<secret>#7",
"shortId": "<secret>#8"
},
"customerId": "<cus>#2",
"id": "<ncl>#2",
"nodeId": "<nod>#2",
@@ -2826,11 +2865,6 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#3",
"relayPassword": "<secret>#5",
"shortId": "<secret>#6"
},
"customerId": "<cus>#1",
"id": "<ncl>#3",
"nodeId": "<nod>#1",
@@ -2839,11 +2873,6 @@
},
{
"createdAt": "<ts>",
"credentials": {
"clientId": "<uuid>#4",
"relayPassword": "<secret>#13",
"shortId": "<secret>#10"
},
"customerId": "<cus>#3",
"id": "<ncl>#4",
"nodeId": "<nod>#3",
@@ -2861,8 +2890,8 @@
"entryReady": true,
"error": null,
"exitCredentials": {
"clientId": "<uuid>#5",
"password": "<secret>#11",
"clientId": "<uuid>#4",
"password": "<secret>#10",
"socksUser": "ng_5b8dd6b991"
},
"exitPort": "<port>#1",
@@ -2909,7 +2938,7 @@
"epoch": "golden-epoch-1",
"uplink": 8000
},
"wsPath": "<secret>#1"
"wsPath": "<secret>#5"
},
{
"artifacts": {},
@@ -2938,12 +2967,12 @@
"retiredUsageEpochs": [],
"serverId": "<srv>#1",
"sortIndex": 1,
"ssServerKey": "<secret>#12",
"ssServerKey": "<secret>#11",
"status": "active",
"topology": "direct",
"updatedAt": "<ts>",
"usageCounters": null,
"wsPath": "<secret>#2"
"wsPath": "<secret>#6"
},
{
"artifacts": {
+72 -63
View File
@@ -603,9 +603,40 @@ func CustomerSummary(data *Data, nowMs int64) map[string]any {
}
}
// credentialItems mirrors credentialItems in server.js.
func credentialItems(d *Data, customer *Customer) []map[string]any {
items := []map[string]any{}
// realityPublicKeyOf returns the node's panel-hosted Reality public key as
// reported back by the agent ("" when unknown). The keypair is the server's
// identity — it stays per-node even though customer credentials are shared.
func realityPublicKeyOf(node *Node) string {
if node == nil {
return ""
}
if raw, ok := node.Artifacts["realityPublicKey"]; ok && len(raw) > 0 {
var s string
if err := json.Unmarshal(raw, &s); err == nil {
return s
}
}
return ""
}
// customerCredentialsView is the credentials-tab payload (round-2 item 2):
// the customer's ONE credential set, plus per-binding node facts. The
// Reality public key is shown per node because it belongs to the server,
// not to the customer.
func customerCredentialsView(d *Data, customer *Customer) map[string]any {
creds := map[string]any{"clientId": nil, "password": nil, "shortId": nil}
if customer.Credentials != nil {
if customer.Credentials.ClientID != "" {
creds["clientId"] = customer.Credentials.ClientID
}
if customer.Credentials.RelayPassword != "" {
creds["password"] = customer.Credentials.RelayPassword
}
if customer.Credentials.ShortID != "" {
creds["shortId"] = customer.Credentials.ShortID
}
}
bindings := []map[string]any{}
for _, nc := range d.NodeClients {
if nc.CustomerID != customer.ID {
continue
@@ -618,30 +649,26 @@ func credentialItems(d *Data, customer *Customer) []map[string]any {
if node.Protocol == "socks5-auth" {
username = NodeEmail(customer.ID)
}
creds := nc.Credentials
var clientID, password, shortID any
if creds.ClientID != "" {
clientID = creds.ClientID
var publicKey any
if key := realityPublicKeyOf(node); key != "" {
publicKey = key
}
if creds.RelayPassword != "" {
password = creds.RelayPassword
}
if creds.ShortID != "" {
shortID = creds.ShortID
}
items = append(items, map[string]any{
"nodeClientId": nc.ID,
"nodeId": node.ID,
"nodeName": node.Name,
"protocol": node.Protocol,
"status": nc.Status,
"clientId": clientID,
"password": password,
"shortId": shortID,
"username": username,
bindings = append(bindings, map[string]any{
"nodeClientId": nc.ID,
"nodeId": node.ID,
"nodeName": node.Name,
"protocol": node.Protocol,
"status": nc.Status,
"username": username,
"realityPublicKey": publicKey,
})
}
return items
return map[string]any{
"customerId": customer.ID,
"subscriptionToken": customer.SubscriptionToken,
"credentials": creds,
"bindings": bindings,
}
}
// CustomerBlockReason is customerBlockReason: "expired" / "traffic_limit" /
@@ -1926,6 +1953,7 @@ func (a *App) handleAdminApi(w http.ResponseWriter, r *http.Request, pathname st
Notes: CleanText(body["notes"], 1000),
CreatedAt: NowISO(),
UpdatedAt: NowISO(),
Credentials: func() *Credentials { c := NewCustomerCredentials(); return &c }(),
}
// Arm the monthly-reset schedule: it starts with the next
// reset instant, it never fires retroactively at creation.
@@ -2131,54 +2159,44 @@ func (a *App) handleAdminApi(w http.ResponseWriter, r *http.Request, pathname st
if customer == nil {
return true, httpErr(404, "客户不存在")
}
SendJSON(w, 200, map[string]any{
"customerId": customer.ID,
"subscriptionToken": customer.SubscriptionToken,
"items": credentialItems(data, customer),
}, nil)
SendJSON(w, 200, customerCredentialsView(data, customer), nil)
return true, nil
}
if params, ok := RoutePattern("/api/customers/:id/credentials/:ncid/regenerate", pathname); ok && r.Method == http.MethodPost {
var responseItem map[string]any
if params, ok := RoutePattern("/api/customers/:id/credentials/regenerate", pathname); ok && r.Method == http.MethodPost {
var view map[string]any
_, err := a.store.Transaction(func(d *Data) (any, error) {
customer := findCustomer(d, params["id"])
if customer == nil {
return nil, httpErr(404, "客户不存在")
}
var binding *NodeClient
// One set for the whole customer: rotating it rotates every
// node at once, and every deployed node is rebuilt so the new
// users land on the agents.
creds := NewCustomerCredentials()
customer.Credentials = &creds
customer.UpdatedAt = NowISO()
rebuilt := map[string]bool{}
for _, nc := range d.NodeClients {
if nc.ID == params["ncid"] && nc.CustomerID == customer.ID {
binding = nc
break
if nc.CustomerID != customer.ID || nc.Status != "active" {
continue
}
}
if binding == nil {
return nil, httpErr(404, "凭据不存在")
}
node := findNode(d, binding.NodeID)
if node == nil {
return nil, httpErr(404, "节点不存在")
}
binding.Credentials = NewNodeClientCredentials(node.Protocol)
binding.UpdatedAt = NowISO()
if node.Status != "draft" && node.Status != "removing" {
node := findNode(d, nc.NodeID)
if node == nil || node.Status == "draft" || node.Status == "removing" || rebuilt[node.ID] {
continue
}
rebuilt[node.ID] = true
if err := RebuildNode(d, node, false); err != nil {
return nil, err
}
}
Audit(d, actor, "regenerate_credentials", customer.ID, map[string]any{"nodeId": node.ID, "node": node.Name})
for _, entry := range credentialItems(d, customer) {
if entry["nodeClientId"] == binding.ID {
responseItem = entry
break
}
}
Audit(d, actor, "regenerate_credentials", customer.ID, map[string]any{"nodesRebuilt": len(rebuilt)})
view = customerCredentialsView(d, customer)
return nil, nil
})
if err != nil {
return true, err
}
SendJSON(w, 200, map[string]any{"item": responseItem}, nil)
SendJSON(w, 200, view, nil)
return true, nil
}
if params, ok := RoutePattern("/api/customers/:id/external-sources/:sid/refresh", pathname); ok && r.Method == http.MethodPost {
@@ -2437,21 +2455,12 @@ func (a *App) handleAdminApi(w http.ResponseWriter, r *http.Request, pathname st
oldServerID := item.ServerID
oldExitServerID := strVal(item.ExitServerID)
oldForward := forwardNode(item)
oldProtocol := item.Protocol
next, err := NormalizeNode(body, item, d)
if err != nil {
return nil, err
}
*item = *next
EnsureNodeSecrets(item)
if item.Protocol != oldProtocol {
for _, nc := range d.NodeClients {
if nc.NodeID == item.ID {
nc.Credentials = NewNodeClientCredentials(item.Protocol)
nc.UpdatedAt = NowISO()
}
}
}
if item.ServerID != oldServerID {
QueueJob(d, oldServerID, item.ID, "delete_resource", &JobPayload{ResourceID: item.ResourceID, Kind: "node"})
item.EntryReady = false
+40 -15
View File
@@ -239,6 +239,16 @@ func payloadWithResource(resource map[string]any, kind string, keyMaterial map[s
return payload
}
// ensureCustomerCredentials fills a missing customer-level credential set
// (defensive: the store-load migration and customer creation provide one).
func ensureCustomerCredentials(d *Data, customerID string) {
customer := findCustomer(d, customerID)
if customer != nil && (customer.Credentials == nil || customer.Credentials.ClientID == "") {
creds := NewCustomerCredentials()
customer.Credentials = &creds
}
}
func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any, error) {
server := orchFindServer(d, node.ServerID)
if server == nil {
@@ -262,11 +272,26 @@ func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any
if clients == nil {
clients = activeNodeClients(d, node)
}
// Credentials live on the customer (round-2 item 2); stored bindings
// carry none. Stamp each customer's set onto throwaway copies for the
// resource builder so the stored records stay clean.
resolved := make([]*NodeClient, len(clients))
for i, nc := range clients {
ensureCustomerCredentials(d, nc.CustomerID)
cp := *nc
if customer := findCustomer(d, nc.CustomerID); customer != nil && customer.Credentials != nil {
creds := *customer.Credentials
cp.Credentials = &creds
} else {
cp.Credentials = nil
}
resolved[i] = &cp
}
return BuildNodeResource(BuildNodeResourceParams{
ResourceID: node.ResourceID,
TagPrefix: tagForNode(node),
Node: node,
NodeClients: clients,
NodeClients: resolved,
Server: server,
BindAddress: bindAddress,
ExitServer: exitServer,
@@ -977,14 +1002,14 @@ func assignCustomerData(d *Data, nodeID, customerID, actor string) (*NodeClient,
nc.UpdatedAt = NowISO()
} else {
now := NowISO()
ensureCustomerCredentials(d, customerID)
nc = &NodeClient{
ID: NewID("ncl"),
NodeID: nodeID,
CustomerID: customerID,
Credentials: NewNodeClientCredentials(node.Protocol),
Status: "active",
CreatedAt: now,
UpdatedAt: now,
ID: NewID("ncl"),
NodeID: nodeID,
CustomerID: customerID,
Status: "active",
CreatedAt: now,
UpdatedAt: now,
}
d.NodeClients = append(d.NodeClients, nc)
}
@@ -1077,14 +1102,14 @@ func AlignCustomerNodes(d *Data, customer *Customer, desiredIds []string, actor
status = "active"
}
now := NowISO()
ensureCustomerCredentials(d, customer.ID)
d.NodeClients = append(d.NodeClients, &NodeClient{
ID: NewID("ncl"),
NodeID: nodeID,
CustomerID: customer.ID,
Credentials: NewNodeClientCredentials(node.Protocol),
Status: status,
CreatedAt: now,
UpdatedAt: now,
ID: NewID("ncl"),
NodeID: nodeID,
CustomerID: customer.ID,
Status: status,
CreatedAt: now,
UpdatedAt: now,
})
Audit(d, actor, "assign_customer", node.ID, map[string]any{"customerId": customer.ID, "customer": customer.Name})
if status == "active" {
+45 -15
View File
@@ -196,18 +196,47 @@ func (q xbQuery) String() string {
func NodeEmail(customerID string) string { return "ng:" + customerID }
func NewNodeClientCredentials(protocol string) Credentials {
keyBytes := 24
if cipher, ok := ShadowsocksMethods[protocol]; ok {
keyBytes = cipher.KeyBytes
}
// NewCustomerCredentials mints the single credential set a customer uses on
// every node (round-2 item 2, 3x-ui model). The relay password is always a
// base64 32-byte root key: SS2022 user keys are derived from it per method
// (see EffectiveRelayPassword), so one set serves aes128 and aes256 nodes
// alike; every other protocol uses it verbatim.
func NewCustomerCredentials() Credentials {
return Credentials{
ClientID: NewUUID(),
RelayPassword: b64(keyBytes),
RelayPassword: b64(32),
ShortID: shortId(),
}
}
// EffectiveRelayPassword maps a customer's relay password onto a node's
// protocol. SS2022 methods require the user key to be base64 of exactly the
// method's key length, so the user key is the deterministic prefix
// truncation of the 32-byte root key. Legacy SS methods and all other
// protocols take the password verbatim.
func EffectiveRelayPassword(protocol, relayPassword string) string {
cipher, ok := ShadowsocksMethods[protocol]
if !ok || !strings.HasPrefix(cipher.Method, "2022-blake3-") {
return relayPassword
}
raw, err := base64.StdEncoding.DecodeString(relayPassword)
if err != nil || len(raw) < cipher.KeyBytes {
return relayPassword
}
return base64.StdEncoding.EncodeToString(raw[:cipher.KeyBytes])
}
// clientCredentials resolves the credentials stamped onto a binding for
// resource building. Stored bindings no longer carry credentials (they live
// on the customer); builders receive resolved copies, and this accessor
// keeps a nil hole from becoming a panic.
func clientCredentials(nc *NodeClient) Credentials {
if nc == nil || nc.Credentials == nil {
return Credentials{}
}
return *nc.Credentials
}
func EnsureNodeSecrets(node *Node) {
if node.WSPath == nil || *node.WSPath == "" {
node.WSPath = StrPtr("/" + hex.EncodeToString(xbRandBytes(9)))
@@ -352,12 +381,13 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
users := make([]any, 0, len(nodeClients))
shortIDs := make([]any, 0, len(nodeClients))
for _, nc := range nodeClients {
user := map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": nc.Credentials.ClientID}
creds := clientCredentials(nc)
user := map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": creds.ClientID}
if protocol == "vless-reality-vision" {
user["flow"] = "xtls-rprx-vision"
}
users = append(users, user)
shortIDs = append(shortIDs, nc.Credentials.ShortID)
shortIDs = append(shortIDs, creds.ShortID)
}
destPort := node.Reality.DestPort
if destPort == 0 {
@@ -383,7 +413,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
}
users := make([]any, 0, len(nodeClients))
for _, nc := range nodeClients {
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": nc.Credentials.ClientID})
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "uuid": clientCredentials(nc).ClientID})
}
wsPath := xbDeref(node.WSPath)
if wsPath == "" {
@@ -406,7 +436,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
if cipher, ok := ShadowsocksMethods[protocol]; ok {
users := make([]any, 0, len(nodeClients))
for _, nc := range nodeClients {
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword})
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": EffectiveRelayPassword(protocol, clientCredentials(nc).RelayPassword)})
}
return map[string]any{
"type": "shadowsocks", "tag": tag, "listen": listen, "listen_port": port,
@@ -417,7 +447,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
if protocol == "socks5-auth" {
users := make([]any, 0, len(nodeClients))
for _, nc := range nodeClients {
users = append(users, map[string]any{"username": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword})
users = append(users, map[string]any{"username": NodeEmail(nc.CustomerID), "password": clientCredentials(nc).RelayPassword})
}
return map[string]any{"type": "socks", "tag": tag, "listen": listen, "listen_port": port, "users": users}, nil
}
@@ -428,7 +458,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
}
users := make([]any, 0, len(nodeClients))
for _, nc := range nodeClients {
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword})
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": clientCredentials(nc).RelayPassword})
}
return map[string]any{
"type": "hysteria2", "tag": tag, "listen": listen, "listen_port": port, "users": users,
@@ -446,7 +476,7 @@ func buildSingBoxInbound(tag string, node *Node, nodeClients []*NodeClient, serv
}
users := make([]any, 0, len(nodeClients))
for _, nc := range nodeClients {
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": nc.Credentials.RelayPassword})
users = append(users, map[string]any{"name": NodeEmail(nc.CustomerID), "password": clientCredentials(nc).RelayPassword})
}
return map[string]any{
"type": "anytls", "tag": tag, "listen": listen, "listen_port": port, "users": users,
@@ -682,9 +712,9 @@ func BuildClientURI(p BuildClientURIParams) (string, bool) {
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", p.ClientID, host, p.RelayPort, q.String(), label), true
}
if cipher, ok := ShadowsocksMethods[protocol]; ok {
password := p.RelayPassword
password := EffectiveRelayPassword(protocol, p.RelayPassword)
if strings.HasPrefix(cipher.Method, "2022-blake3-") && p.SSServerKey != "" {
password = p.SSServerKey + ":" + p.RelayPassword
password = p.SSServerKey + ":" + password
}
user := base64.RawURLEncoding.EncodeToString([]byte(cipher.Method + ":" + password))
return fmt.Sprintf("ss://%s@%s:%d#%s", user, host, p.RelayPort, label), true
+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"},
}
rfClients := []*NodeClient{
{NodeID: "nod_rf", CustomerID: "cus_1", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "unused1", ShortID: "abcd0001ef01"}},
{NodeID: "nod_rf", CustomerID: "cus_2", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000002", RelayPassword: "unused2", ShortID: "abcd0002ef01"}},
{NodeID: "nod_rf", CustomerID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "unused1", ShortID: "abcd0001ef01"}},
{NodeID: "nod_rf", CustomerID: "cus_2", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000002", RelayPassword: "unused2", ShortID: "abcd0002ef01"}},
}
entryServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.10"}
exitServer := &Server{ID: "srv_exit", PublicAddress: "exit.example.com"}
@@ -102,7 +102,7 @@ func TestGoldenResources(t *testing.T) {
SSServerKey: StrPtr("CwwNDg8QERITFBUWFxgZGg=="),
}
ssClients := []*NodeClient{
{NodeID: "nod_ss", CustomerID: "cus_1", Credentials: Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01"}},
{NodeID: "nod_ss", CustomerID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01"}},
}
ssServer := &Server{ID: "srv_entry", PublicAddress: "203.0.113.11"}
+61
View File
@@ -6,6 +6,7 @@ package main
// the same serialization here.
import (
"encoding/base64"
"errors"
"fmt"
"os"
@@ -133,10 +134,70 @@ func (s *Store) Init() error {
if err := validateData(&data); err != nil {
return err
}
if MigrateCustomerCredentials(&data) {
// Persist the converged credential model immediately, so a crash
// before the next transaction cannot resurrect per-binding sets.
if err := s.persist(&data); err != nil {
return err
}
}
s.Data = &data
return nil
}
// MigrateCustomerCredentials converges legacy per-binding credentials into
// each customer's single set (round-2 item 2; the 3x-ui model of one
// credential row per client). Deterministic rules:
// - A customer with a complete stored set keeps it.
// - Otherwise the set is copied from the customer's first binding (in
// stored order) that carries credentials; a customer with no usable
// binding credentials gets a fresh set.
// - The relay password must be a base64 32-byte root key, because SS2022
// user keys derive from it by prefix truncation (EffectiveRelayPassword).
// If the converged password does not decode to exactly 32 bytes, only
// the password is regenerated — clientId and shortId are preserved.
// - Every binding's stored credentials are then cleared, so bindings stop
// carrying credentials at all.
//
// Returns true when anything changed.
func MigrateCustomerCredentials(d *Data) bool {
changed := false
for _, customer := range d.Customers {
var creds *Credentials
if customer.Credentials != nil && customer.Credentials.ClientID != "" {
copied := *customer.Credentials
creds = &copied
} else {
for _, nc := range d.NodeClients {
if nc.CustomerID == customer.ID && nc.Credentials != nil && nc.Credentials.ClientID != "" {
copied := *nc.Credentials
creds = &copied
break
}
}
}
if creds == nil {
fresh := NewCustomerCredentials()
creds = &fresh
}
if raw, err := base64.StdEncoding.DecodeString(creds.RelayPassword); err != nil || len(raw) != 32 {
creds.RelayPassword = b64(32)
}
old := customer.Credentials
if old == nil || old.ClientID != creds.ClientID || old.RelayPassword != creds.RelayPassword || old.ShortID != creds.ShortID {
customer.Credentials = creds
changed = true
}
}
for _, nc := range d.NodeClients {
if nc.Credentials != nil {
nc.Credentials = nil
changed = true
}
}
return changed
}
// Snapshot returns a deep copy of the current data (JS structuredClone).
func (s *Store) Snapshot() *Data {
s.mu.Lock()
+7 -3
View File
@@ -108,13 +108,17 @@ func NodeClientURI(d *Data, nc *NodeClient) string {
if networkMode == "" {
networkMode = "ipv4"
}
creds := Credentials{}
if customer := findCustomer(d, nc.CustomerID); customer != nil && customer.Credentials != nil {
creds = *customer.Credentials
}
uri, ok := BuildClientURI(BuildClientURIParams{
Protocol: node.Protocol,
RelayServer: server,
RelayPort: node.Port,
ClientID: nc.Credentials.ClientID,
RelayPassword: nc.Credentials.RelayPassword,
ShortID: nc.Credentials.ShortID,
ClientID: creds.ClientID,
RelayPassword: creds.RelayPassword,
ShortID: creds.ShortID,
WSPath: xbDeref(node.WSPath),
SocksUser: NodeEmail(nc.CustomerID),
Reality: node.Reality,
+12 -13
View File
@@ -36,26 +36,25 @@ func goldenSubData() *Data {
Reality: &RealityConfig{KeyID: "reality-nod_rv", ServerName: "www.apple.com", DestPort: 443},
Artifacts: art("FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu")},
}
nc := func(nodeID, clientID, relayPassword, shortID string) *NodeClient {
return &NodeClient{
NodeID: nodeID, CustomerID: "cus_1", Status: "active",
Credentials: Credentials{ClientID: clientID, RelayPassword: relayPassword, ShortID: shortID},
}
nc := func(nodeID string) *NodeClient {
return &NodeClient{NodeID: nodeID, CustomerID: "cus_1", Status: "active"}
}
clients := []*NodeClient{
nc("nod_rf", "11111111-2222-3333-4444-000000000001", "unused", "abcd0001ef01"),
nc("nod_ss", "11111111-2222-3333-4444-000000000003", "FRYXGBkaGxwdHh8gISIjJA==", "abcd0003ef01"),
nc("nod_hy", "11111111-2222-3333-4444-000000000004", "HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz", "abcd0004ef01"),
nc("nod_any", "11111111-2222-3333-4444-000000000005", "IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6", "abcd0005ef01"),
nc("nod_ws", "11111111-2222-3333-4444-000000000006", "unused", "abcd0006ef01"),
nc("nod_sk", "11111111-2222-3333-4444-000000000007", "MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI", "abcd0007ef01"),
nc("nod_rv", "11111111-2222-3333-4444-000000000008", "unused", "abcd0008ef01"),
nc("nod_rf"), nc("nod_ss"), nc("nod_hy"), nc("nod_any"),
nc("nod_ws"), nc("nod_sk"), nc("nod_rv"),
}
return &Data{
Servers: []*Server{entryServer, v6Server},
Nodes: nodes,
NodeClients: clients,
Customers: []*Customer{{ID: "cus_1"}},
// One credential set for the whole customer (round-2 item 2). The
// values are the old RF binding's uuid/shortId and the old SS
// binding's password, so the RF and SS entries are byte-stable
// against the previous golden file; every other entry now carries
// this same set.
Customers: []*Customer{{ID: "cus_1", Credentials: &Credentials{
ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "FRYXGBkaGxwdHh8gISIjJA==", ShortID: "abcd0001ef01",
}}},
}
}
+32 -32
View File
@@ -2,7 +2,7 @@
"entries": [
{
"nodeId": "nod_rf",
"clientUri": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu&sid=abcd0001ef01&type=tcp&headerType=none&flow=xtls-rprx-vision#RF-Reality"
"clientUri": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none\u0026security=reality\u0026sni=www.amd.com\u0026fp=chrome\u0026pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none\u0026flow=xtls-rprx-vision#RF-Reality"
},
{
"nodeId": "nod_ss",
@@ -10,56 +10,56 @@
},
{
"nodeId": "nod_hy",
"clientUri": "hysteria2://HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz@entry.example.com:23459?sni=tls.example.com&alpn=h3#HY2"
"clientUri": "hysteria2://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23459?sni=tls.example.com\u0026alpn=h3#HY2"
},
{
"nodeId": "nod_any",
"clientUri": "anytls://IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6@entry.example.com:23460?sni=tls.example.com#AnyTLS"
"clientUri": "anytls://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23460?sni=tls.example.com#AnyTLS"
},
{
"nodeId": "nod_ws",
"clientUri": "vless://11111111-2222-3333-4444-000000000006@entry.example.com:23461?encryption=none&security=tls&type=ws&path=%2Fwspath123&sni=tls.example.com#WS-TLS"
"clientUri": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23461?encryption=none\u0026security=tls\u0026type=ws\u0026path=%2Fwspath123\u0026sni=tls.example.com#WS-TLS"
},
{
"nodeId": "nod_sk",
"clientUri": "socks5://ng%3Acus_1:MTIzNDU2Nzg5Ojs8PT4%2FQEFCQ0RFRkdI@entry.example.com:23462#Socks"
"clientUri": "socks5://ng%3Acus_1:FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23462#Socks"
},
{
"nodeId": "nod_rv",
"clientUri": "vless://11111111-2222-3333-4444-000000000008@v6.example.com:23463?encryption=none&security=reality&sni=www.apple.com&fp=chrome&pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu&sid=abcd0008ef01&type=tcp&headerType=none#RV-Plain"
"clientUri": "vless://11111111-2222-3333-4444-000000000001@v6.example.com:23463?encryption=none\u0026security=reality\u0026sni=www.apple.com\u0026fp=chrome\u0026pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none#RV-Plain"
}
],
"formats": {
"raw": {
"body": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none&security=reality&sni=www.amd.com&fp=chrome&pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu&sid=abcd0001ef01&type=tcp&headerType=none&flow=xtls-rprx-vision#RF-Reality\nss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Q3d3TkRnOFFFUklURkJVV0Z4Z1pHZz09OkZSWVhHQmthR3h3ZEhoOGdJU0lqSkE9PQ@entry.example.com:23458#SS-Direct\nhysteria2://HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz@entry.example.com:23459?sni=tls.example.com&alpn=h3#HY2\nanytls://IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6@entry.example.com:23460?sni=tls.example.com#AnyTLS\nvless://11111111-2222-3333-4444-000000000006@entry.example.com:23461?encryption=none&security=tls&type=ws&path=%2Fwspath123&sni=tls.example.com#WS-TLS\nsocks5://ng%3Acus_1:MTIzNDU2Nzg5Ojs8PT4%2FQEFCQ0RFRkdI@entry.example.com:23462#Socks\nvless://11111111-2222-3333-4444-000000000008@v6.example.com:23463?encryption=none&security=reality&sni=www.apple.com&fp=chrome&pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu&sid=abcd0008ef01&type=tcp&headerType=none#RV-Plain\n",
"contentType": "text/plain; charset=utf-8"
},
"base64": {
"body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vSEIwZUh5QWhJaU1rSlNZbktDa3FLeXd0TGk4d01USXpAZW50cnkuZXhhbXBsZS5jb206MjM0NTk/c25pPXRscy5leGFtcGxlLmNvbSZhbHBuPWgzI0hZMgphbnl0bHM6Ly9JeVFsSmljb0tTb3JMQzB1THpBeE1qTTBOVFkzT0RrNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MD9zbmk9dGxzLmV4YW1wbGUuY29tI0FueVRMUwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MT9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9dGxzJnR5cGU9d3MmcGF0aD0lMkZ3c3BhdGgxMjMmc25pPXRscy5leGFtcGxlLmNvbSNXUy1UTFMKc29ja3M1Oi8vbmclM0FjdXNfMTpNVEl6TkRVMk56ZzVPanM4UFQ0JTJGUUVGQ1EwUkZSa2RJQGVudHJ5LmV4YW1wbGUuY29tOjIzNDYyI1NvY2tzCnZsZXNzOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtMDAwMDAwMDAwMDA4QHY2LmV4YW1wbGUuY29tOjIzNDYzP2VuY3J5cHRpb249bm9uZSZzZWN1cml0eT1yZWFsaXR5JnNuaT13d3cuYXBwbGUuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSdktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDA4ZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUjUlYtUGxhaW4K",
"contentType": "text/plain; charset=utf-8"
},
"clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000006\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000008\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0008ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8"
},
"clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000006\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000008\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0008ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8"
},
"singbox": {
"body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\",\n \"node-2\",\n \"node-3\",\n \"node-4\",\n \"node-5\",\n \"node-6\",\n \"node-7\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23456,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0001ef01\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23458,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n },\n {\n \"type\": \"hysteria2\",\n \"tag\": \"node-3\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23459,\n \"password\": \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\",\n \"alpn\": [\n \"h3\"\n ]\n }\n },\n {\n \"type\": \"anytls\",\n \"tag\": \"node-4\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23460,\n \"password\": \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-5\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23461,\n \"uuid\": \"11111111-2222-3333-4444-000000000006\",\n \"transport\": {\n \"type\": \"ws\",\n \"path\": \"/wspath123\"\n },\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"socks\",\n \"tag\": \"node-6\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23462,\n \"version\": \"5\",\n \"username\": \"ng:cus_1\",\n \"password\": \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-7\",\n \"server\": \"v6.example.com\",\n \"server_port\": 23463,\n \"uuid\": \"11111111-2222-3333-4444-000000000008\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.apple.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0008ef01\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n",
"contentType": "application/json; charset=utf-8"
},
"surge": {
"body": "[General]\nloglevel = notify\n[Proxy]\nSS-Direct = ss, entry.example.com, 23458, encrypt-method=2022-blake3-aes-128-gcm, password=CwwNDg8QERITFBUWFxgZGg__:FRYXGBkaGxwdHh8gISIjJA__\nSocks = socks5, entry.example.com, 23462, ng:cus_1, MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\n[Proxy Group]\n节点选择 = select, SS-Direct, Socks, DIRECT\n自动选择 = url-test, SS-Direct, Socks, url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n",
"contentType": "text/plain; charset=utf-8"
},
"auto-clash-ua": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000006\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000008\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0008ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n",
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8"
},
"auto-plain-ua": {
"body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vSEIwZUh5QWhJaU1rSlNZbktDa3FLeXd0TGk4d01USXpAZW50cnkuZXhhbXBsZS5jb206MjM0NTk/c25pPXRscy5leGFtcGxlLmNvbSZhbHBuPWgzI0hZMgphbnl0bHM6Ly9JeVFsSmljb0tTb3JMQzB1THpBeE1qTTBOVFkzT0RrNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MD9zbmk9dGxzLmV4YW1wbGUuY29tI0FueVRMUwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwNkBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MT9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9dGxzJnR5cGU9d3MmcGF0aD0lMkZ3c3BhdGgxMjMmc25pPXRscy5leGFtcGxlLmNvbSNXUy1UTFMKc29ja3M1Oi8vbmclM0FjdXNfMTpNVEl6TkRVMk56ZzVPanM4UFQ0JTJGUUVGQ1EwUkZSa2RJQGVudHJ5LmV4YW1wbGUuY29tOjIzNDYyI1NvY2tzCnZsZXNzOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtMDAwMDAwMDAwMDA4QHY2LmV4YW1wbGUuY29tOjIzNDYzP2VuY3J5cHRpb249bm9uZSZzZWN1cml0eT1yZWFsaXR5JnNuaT13d3cuYXBwbGUuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSdktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDA4ZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUjUlYtUGxhaW4K",
"body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vRlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OT9zbmk9dGxzLmV4YW1wbGUuY29tJmFscG49aDMjSFkyCmFueXRsczovL0ZSWVhHQmthR3h3ZEhoOGdJU0lqSkElM0QlM0RAZW50cnkuZXhhbXBsZS5jb206MjM0NjA/c25pPXRscy5leGFtcGxlLmNvbSNBbnlUTFMKdmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NjE/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXRscyZ0eXBlPXdzJnBhdGg9JTJGd3NwYXRoMTIzJnNuaT10bHMuZXhhbXBsZS5jb20jV1MtVExTCnNvY2tzNTovL25nJTNBY3VzXzE6RlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MiNTb2Nrcwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwMUB2Ni5leGFtcGxlLmNvbToyMzQ2Mz9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9cmVhbGl0eSZzbmk9d3d3LmFwcGxlLmNvbSZmcD1jaHJvbWUmcGJrPUZJWEVEUEJLUnZLZXkwMTIzNDU2Nzg5YWJjZGVmZ2hpamtsbW5vcHFyc3R1JnNpZD1hYmNkMDAwMWVmMDEmdHlwZT10Y3AmaGVhZGVyVHlwZT1ub25lI1JWLVBsYWluCg==",
"contentType": "text/plain; charset=utf-8"
},
"base64": {
"body": "dmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NTY/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXJlYWxpdHkmc25pPXd3dy5hbWQuY29tJmZwPWNocm9tZSZwYms9RklYRURQQktSZktleTAxMjM0NTY3ODlhYmNkZWZnaGlqa2xtbm9wcXJzdHUmc2lkPWFiY2QwMDAxZWYwMSZ0eXBlPXRjcCZoZWFkZXJUeXBlPW5vbmUmZmxvdz14dGxzLXJwcngtdmlzaW9uI1JGLVJlYWxpdHkKc3M6Ly9NakF5TWkxaWJHRnJaVE10WVdWekxURXlPQzFuWTIwNlEzZDNUa1JuT0ZGRlVrbFVSa0pWVjBaNFoxcEhaejA5T2taU1dWaEhRbXRoUjNoM1pFaG9PR2RKVTBscVNrRTlQUUBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OCNTUy1EaXJlY3QKaHlzdGVyaWEyOi8vRlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ1OT9zbmk9dGxzLmV4YW1wbGUuY29tJmFscG49aDMjSFkyCmFueXRsczovL0ZSWVhHQmthR3h3ZEhoOGdJU0lqSkElM0QlM0RAZW50cnkuZXhhbXBsZS5jb206MjM0NjA/c25pPXRscy5leGFtcGxlLmNvbSNBbnlUTFMKdmxlc3M6Ly8xMTExMTExMS0yMjIyLTMzMzMtNDQ0NC0wMDAwMDAwMDAwMDFAZW50cnkuZXhhbXBsZS5jb206MjM0NjE/ZW5jcnlwdGlvbj1ub25lJnNlY3VyaXR5PXRscyZ0eXBlPXdzJnBhdGg9JTJGd3NwYXRoMTIzJnNuaT10bHMuZXhhbXBsZS5jb20jV1MtVExTCnNvY2tzNTovL25nJTNBY3VzXzE6RlJZWEdCa2FHeHdkSGg4Z0lTSWpKQSUzRCUzREBlbnRyeS5leGFtcGxlLmNvbToyMzQ2MiNTb2Nrcwp2bGVzczovLzExMTExMTExLTIyMjItMzMzMy00NDQ0LTAwMDAwMDAwMDAwMUB2Ni5leGFtcGxlLmNvbToyMzQ2Mz9lbmNyeXB0aW9uPW5vbmUmc2VjdXJpdHk9cmVhbGl0eSZzbmk9d3d3LmFwcGxlLmNvbSZmcD1jaHJvbWUmcGJrPUZJWEVEUEJLUnZLZXkwMTIzNDU2Nzg5YWJjZGVmZ2hpamtsbW5vcHFyc3R1JnNpZD1hYmNkMDAwMWVmMDEmdHlwZT10Y3AmaGVhZGVyVHlwZT1ub25lI1JWLVBsYWluCg==",
"contentType": "text/plain; charset=utf-8"
},
"clash": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8"
},
"clash-smart": {
"body": "mixed-port: 7890\nallow-lan: false\nmode: rule\nproxies:\n - name: \"RF-Reality\"\n server: \"entry.example.com\"\n port: 23456\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.amd.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\n flow: \"xtls-rprx-vision\"\n - name: \"SS-Direct\"\n server: \"entry.example.com\"\n port: 23458\n type: \"ss\"\n cipher: \"2022-blake3-aes-128-gcm\"\n password: \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"HY2\"\n server: \"entry.example.com\"\n port: 23459\n type: \"hysteria2\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n alpn: [\"h3\"]\n - name: \"AnyTLS\"\n server: \"entry.example.com\"\n port: 23460\n type: \"anytls\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n sni: \"tls.example.com\"\n udp: true\n - name: \"WS-TLS\"\n server: \"entry.example.com\"\n port: 23461\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"ws\"\n ws-opts: \n path: \"/wspath123\"\n servername: \"tls.example.com\"\n - name: \"Socks\"\n server: \"entry.example.com\"\n port: 23462\n type: \"socks5\"\n username: \"ng:cus_1\"\n password: \"FRYXGBkaGxwdHh8gISIjJA==\"\n - name: \"RV-Plain\"\n server: \"v6.example.com\"\n port: 23463\n type: \"vless\"\n uuid: \"11111111-2222-3333-4444-000000000001\"\n tls: true\n network: \"tcp\"\n servername: \"www.apple.com\"\n client-fingerprint: \"chrome\"\n reality-opts: \n public-key: \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\"\n short-id: \"abcd0001ef01\"\nproxy-groups:\n - name: \"节点选择\"\n type: select\n proxies:\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - \"DIRECT\"\n - name: \"自动选择\"\n type: url-test\n url: \"https://www.gstatic.com/generate_204\"\n interval: 300\n tolerance: 80\n proxies:\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"流媒体\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"AI 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\n - name: \"Meta 服务\"\n type: select\n proxies:\n - \"节点选择\"\n - \"自动选择\"\n - \"RF-Reality\"\n - \"SS-Direct\"\n - \"HY2\"\n - \"AnyTLS\"\n - \"WS-TLS\"\n - \"Socks\"\n - \"RV-Plain\"\nrules:\n - \"GEOSITE,category-ads-all,REJECT\"\n - \"GEOSITE,private,DIRECT\"\n - \"GEOSITE,openai,AI 服务\"\n - \"GEOSITE,netflix,流媒体\"\n - \"GEOSITE,facebook,Meta 服务\"\n - \"DOMAIN-SUFFIX,fbcdn.net,Meta 服务\"\n - \"DOMAIN-SUFFIX,instagram.com,Meta 服务\"\n - \"DOMAIN-SUFFIX,whatsapp.net,Meta 服务\"\n - \"GEOSITE,geolocation-!cn,节点选择\"\n - \"GEOSITE,cn,DIRECT\"\n - \"GEOIP,CN,DIRECT\"\n - \"MATCH,节点选择\"\n",
"contentType": "text/yaml; charset=utf-8"
},
"raw": {
"body": "vless://11111111-2222-3333-4444-000000000001@entry.example.com:23456?encryption=none\u0026security=reality\u0026sni=www.amd.com\u0026fp=chrome\u0026pbk=FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none\u0026flow=xtls-rprx-vision#RF-Reality\nss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Q3d3TkRnOFFFUklURkJVV0Z4Z1pHZz09OkZSWVhHQmthR3h3ZEhoOGdJU0lqSkE9PQ@entry.example.com:23458#SS-Direct\nhysteria2://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23459?sni=tls.example.com\u0026alpn=h3#HY2\nanytls://FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23460?sni=tls.example.com#AnyTLS\nvless://11111111-2222-3333-4444-000000000001@entry.example.com:23461?encryption=none\u0026security=tls\u0026type=ws\u0026path=%2Fwspath123\u0026sni=tls.example.com#WS-TLS\nsocks5://ng%3Acus_1:FRYXGBkaGxwdHh8gISIjJA%3D%3D@entry.example.com:23462#Socks\nvless://11111111-2222-3333-4444-000000000001@v6.example.com:23463?encryption=none\u0026security=reality\u0026sni=www.apple.com\u0026fp=chrome\u0026pbk=FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\u0026sid=abcd0001ef01\u0026type=tcp\u0026headerType=none#RV-Plain\n",
"contentType": "text/plain; charset=utf-8"
},
"singbox": {
"body": "{\n \"log\": {\n \"level\": \"warn\"\n },\n \"inbounds\": [\n {\n \"type\": \"mixed\",\n \"tag\": \"local\",\n \"listen\": \"127.0.0.1\",\n \"listen_port\": 2080\n }\n ],\n \"outbounds\": [\n {\n \"type\": \"selector\",\n \"tag\": \"proxy\",\n \"outbounds\": [\n \"node-1\",\n \"node-2\",\n \"node-3\",\n \"node-4\",\n \"node-5\",\n \"node-6\",\n \"node-7\"\n ]\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-1\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23456,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"flow\": \"xtls-rprx-vision\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.amd.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0001ef01\"\n }\n }\n },\n {\n \"type\": \"shadowsocks\",\n \"tag\": \"node-2\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23458,\n \"method\": \"2022-blake3-aes-128-gcm\",\n \"password\": \"CwwNDg8QERITFBUWFxgZGg==:FRYXGBkaGxwdHh8gISIjJA==\"\n },\n {\n \"type\": \"hysteria2\",\n \"tag\": \"node-3\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23459,\n \"password\": \"FRYXGBkaGxwdHh8gISIjJA==\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\",\n \"alpn\": [\n \"h3\"\n ]\n }\n },\n {\n \"type\": \"anytls\",\n \"tag\": \"node-4\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23460,\n \"password\": \"FRYXGBkaGxwdHh8gISIjJA==\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-5\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23461,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"transport\": {\n \"type\": \"ws\",\n \"path\": \"/wspath123\"\n },\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"tls.example.com\"\n }\n },\n {\n \"type\": \"socks\",\n \"tag\": \"node-6\",\n \"server\": \"entry.example.com\",\n \"server_port\": 23462,\n \"version\": \"5\",\n \"username\": \"ng:cus_1\",\n \"password\": \"FRYXGBkaGxwdHh8gISIjJA==\"\n },\n {\n \"type\": \"vless\",\n \"tag\": \"node-7\",\n \"server\": \"v6.example.com\",\n \"server_port\": 23463,\n \"uuid\": \"11111111-2222-3333-4444-000000000001\",\n \"tls\": {\n \"enabled\": true,\n \"server_name\": \"www.apple.com\",\n \"reality\": {\n \"enabled\": true,\n \"public_key\": \"FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu\",\n \"short_id\": \"abcd0001ef01\"\n }\n }\n },\n {\n \"type\": \"direct\",\n \"tag\": \"direct\"\n }\n ],\n \"route\": {\n \"final\": \"proxy\"\n }\n}\n",
"contentType": "application/json; charset=utf-8"
},
"surge": {
"body": "[General]\nloglevel = notify\n[Proxy]\nSS-Direct = ss, entry.example.com, 23458, encrypt-method=2022-blake3-aes-128-gcm, password=CwwNDg8QERITFBUWFxgZGg__:FRYXGBkaGxwdHh8gISIjJA__\nSocks = socks5, entry.example.com, 23462, ng:cus_1, FRYXGBkaGxwdHh8gISIjJA__\n[Proxy Group]\n节点选择 = select, SS-Direct, Socks, DIRECT\n自动选择 = url-test, SS-Direct, Socks, url=http://www.gstatic.com/generate_204, interval=600\n[Rule]\nGEOIP,CN,DIRECT\nFINAL,节点选择\n",
"contentType": "text/plain; charset=utf-8"
}
}
+19 -11
View File
@@ -560,9 +560,13 @@ type Customer struct {
ServerUsage map[string]*UsageSlot `json:"serverUsage,omitempty"`
LastUsageAt *string `json:"lastUsageAt,omitempty"`
LastOnlineAt *string `json:"lastOnlineAt,omitempty"`
ExternalSources []ExtSource `json:"externalSources,omitempty"`
ManualLinks []ManualLink `json:"manualLinks,omitempty"`
Extra extraFields `json:"-"`
// The customer's single credential set, shared by all of their node
// bindings (round-2 item 2). Populated by the store-load migration for
// legacy records and at creation for new ones.
Credentials *Credentials `json:"credentials,omitempty"`
ExternalSources []ExtSource `json:"externalSources,omitempty"`
ManualLinks []ManualLink `json:"manualLinks,omitempty"`
Extra extraFields `json:"-"`
}
// ExtSource is one external subscription source on a customer
@@ -781,14 +785,18 @@ func (c Credentials) MarshalJSON() ([]byte, error) {
}
type NodeClient struct {
ID string `json:"id"`
NodeID string `json:"nodeId"`
CustomerID string `json:"customerId"`
Credentials Credentials `json:"credentials"`
Status string `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Extra extraFields `json:"-"`
ID string `json:"id"`
NodeID string `json:"nodeId"`
CustomerID string `json:"customerId"`
// Deprecated: credentials live on the Customer (round-2 item 2). This
// field is only a migration input — the store-load migration converges
// it into Customer.Credentials and clears it, so it disappears from
// disk on the next save. Builders receive resolved copies instead.
Credentials *Credentials `json:"credentials,omitempty"`
Status string `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Extra extraFields `json:"-"`
}
func (n *NodeClient) UnmarshalJSON(raw []byte) error {
+4 -3
View File
@@ -53,8 +53,8 @@ func TestAggregateEntriesOrderAndDedup(t *testing.T) {
},
Servers: []*Server{{ID: "srv_1", Name: "S", PublicAddress: "s.example.com"}},
NodeClients: []*NodeClient{
{ID: "ncl_b", NodeID: "nod_b", CustomerID: "cus_1", Status: "active", Credentials: Credentials{RelayPassword: "pw-b"}},
{ID: "ncl_a", NodeID: "nod_a", CustomerID: "cus_1", Status: "active", Credentials: Credentials{RelayPassword: "pw-a"}},
{ID: "ncl_b", NodeID: "nod_b", CustomerID: "cus_1", Status: "active"},
{ID: "ncl_a", NodeID: "nod_a", CustomerID: "cus_1", Status: "active"},
},
SubSourceCache: map[string]*SubSourceCacheEntry{
"src_1": {Items: []SubCacheItem{{URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Dup", Name: "Dup"}}},
@@ -63,12 +63,13 @@ func TestAggregateEntriesOrderAndDedup(t *testing.T) {
okAt := NowISO()
d.SubSourceCache["src_1"].OkAt = &okAt
customer := &Customer{
ID: "cus_1",
ID: "cus_1", Credentials: &Credentials{ClientID: "11111111-2222-3333-4444-000000000001", RelayPassword: "pw-a", ShortID: "abcd0001ef01"},
ManualLinks: []ManualLink{
{ID: "lnk_1", URI: "ss://YWVzLTI1Ni1nY206cHctYQ@s.example.com:20002#Whatever", Remark: "手动一号", Enabled: true},
},
ExternalSources: []ExtSource{{ID: "src_1", URL: "https://example.com/sub", Remark: "机场", Enabled: true}},
}
d.Customers = []*Customer{customer}
entries := AggregateEntries(d, customer)
names := []string{}
for _, entry := range entries {