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:
+11
-23
@@ -345,21 +345,12 @@ func cancelQueuedDeletes(d *Data, nodeID, reason string) bool {
|
||||
}
|
||||
|
||||
func RebuildNode(d *Data, node *Node, includeExit bool) error {
|
||||
// Round-3 item 20: a node whose bindings drop to zero is rebuilt
|
||||
// like any other (its inbound just ends up with no customer users);
|
||||
// it is NOT auto-removed. The pre-item-20 auto-removal branch is
|
||||
// gone; the AutoRemoving revival paths in assign/resume stay for
|
||||
// records an older panel left in that state.
|
||||
clients := activeNodeClients(d, node)
|
||||
if len(clients) == 0 {
|
||||
if node.Status != "draft" && node.Status != "removing" {
|
||||
QueueJob(d, node.ServerID, node.ID, "delete_resource", &JobPayload{ResourceID: node.ResourceID, Kind: "node"})
|
||||
if forwardNode(node) && node.ExitResourceID != "" {
|
||||
QueueJob(d, xbDeref(node.ExitServerID), node.ID, "delete_resource", &JobPayload{ResourceID: node.ExitResourceID, Kind: "node-exit"})
|
||||
}
|
||||
node.Status = "removing"
|
||||
node.AutoRemoving = BoolPtr(true)
|
||||
node.PendingDelete = false
|
||||
node.Error = nil
|
||||
}
|
||||
node.UpdatedAt = NowISO()
|
||||
return nil
|
||||
}
|
||||
node.AutoRemoving = BoolPtr(false)
|
||||
resource, err := resourceForNode(d, node, clients)
|
||||
if err != nil {
|
||||
@@ -657,9 +648,12 @@ func ValidateNodeDeploy(d *Data, node *Node) error {
|
||||
return httpErr(409, "设备 %s 已停用", target.Name)
|
||||
}
|
||||
}
|
||||
if len(activeNodeClients(d, node)) == 0 {
|
||||
return httpErr(400, "请先给节点分配至少一个客户再部署")
|
||||
}
|
||||
// Round-3 item 20: no client-count gate. Nodes are built and deployed
|
||||
// first and bound later (the 3x-ui model); bindings may go to zero
|
||||
// and back without tearing the node down. A zero-client inbound
|
||||
// simply has no users who can connect — including socks5-auth,
|
||||
// whose inbound always carries its node-level fixed account
|
||||
// (item 21), so it never degenerates into an open proxy.
|
||||
switch node.Protocol {
|
||||
case "vless-ws-tls", "hysteria2", "anytls":
|
||||
if server.TLSDomain == "" {
|
||||
@@ -807,9 +801,6 @@ func reconcileServerData(d *Data, serverID, actor string, opts ReconcileOptions)
|
||||
}
|
||||
} else {
|
||||
clients := activeNodeClients(d, node)
|
||||
if len(clients) == 0 {
|
||||
continue
|
||||
}
|
||||
if isEntry {
|
||||
resource, err := resourceForNode(d, node, clients)
|
||||
if err != nil {
|
||||
@@ -1217,9 +1208,6 @@ func (o *Orchestrator) ReconcileFromHeartbeat(serverID string, reportedResourceI
|
||||
if node.Status == "draft" || node.Status == "removing" {
|
||||
continue
|
||||
}
|
||||
if len(activeNodeClients(d, node)) == 0 {
|
||||
continue
|
||||
}
|
||||
if node.ServerID == serverID && node.ResourceID != "" && !reported[node.ResourceID] {
|
||||
missing[node.ID] = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user