feat(panel): build-before-bind + socks5 fixed node account (items 20+21)

Item 20: drop the zero-client deploy gate in ValidateNodeDeploy for
all protocols, stop RebuildNode from auto-removing a node whose
bindings reach zero, and let server/heartbeat reconciliation rebuild
zero-client nodes like any other. Nodes are built and deployed first;
bindings can be added and removed freely.

Item 21: socks5-auth nodes carry one node-level fixed account
(node.socksUser/socksPassword, minted at creation or by the store-load
migration MigrateSocksNodeCredentials, idempotent). The sing-box
inbound's only user is that pair regardless of bindings; subscription
links for every bound customer carry the same pair. The generated
username avoids the ng: prefix so the agent never attributes the
account's traffic to a customer — it stays on the node's inbound
counters. Edit form shows both fields (copyable) for socks nodes;
NormalizeNode validates operator-supplied usernames.
This commit is contained in:
aaaamoooo
2026-10-21 10:40:58 +00:00
parent bb974e2ed7
commit 2fafd85d1f
9 changed files with 297 additions and 34 deletions
+26 -4
View File
@@ -236,7 +236,27 @@ func clientCredentials(nc *NodeClient) Credentials {
return *nc.Credentials
}
// EnsureSocksCredentials mints the node-level fixed account of a
// socks5-auth node (round-3 item 21) when either half is missing. The
// username deliberately avoids the "ng:" prefix: the agent maps sing-box
// user counters named "ng:<customerId>" to per-customer usage samples,
// and the fixed account's traffic must stay node-level (the "inbound"
// sample), never attributed to a customer. The password is a base64
// 32-byte random, the same strength as a customer's relay password.
func EnsureSocksCredentials(node *Node) {
if node.Protocol != "socks5-auth" {
return
}
if node.SocksUser == "" {
node.SocksUser = "ngs_" + hex.EncodeToString(xbRandBytes(5))
}
if node.SocksPassword == "" {
node.SocksPassword = b64(32)
}
}
func EnsureNodeSecrets(node *Node) {
EnsureSocksCredentials(node)
if node.WSPath == nil || *node.WSPath == "" {
node.WSPath = StrPtr("/" + hex.EncodeToString(xbRandBytes(9)))
}
@@ -409,10 +429,12 @@ 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": clientCredentials(nc).RelayPassword})
}
// Round-3 item 21: the inbound carries exactly one user — the
// node's fixed account — regardless of bindings. Bindings only
// control subscription visibility; per-person accounting for
// relay chains happens at the entry side, not here.
EnsureSocksCredentials(node)
users := []any{map[string]any{"username": node.SocksUser, "password": node.SocksPassword}}
return map[string]any{"type": "socks", "tag": tag, "listen": listen, "listen_port": port, "users": users}, nil
}