mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-22 12:36:07 +00:00
fix(nodes): keep cloned nodes (shared panelGuid) in separate attribution buckets
#4983 keys online/inbound attribution by panelGuid, assuming it is globally unique. Cloned node servers ship an identical panelGuid in their copied settings, so the master collapsed several physical nodes into one bucket: GetMergedNodeTrees merged their online sets under one key and every inbound on those nodes (same origin_node_guid) read that merged set, so the inbound page showed online cross-attributed and counts inflated. Fall back to the node-unique synthNodeGuid(node.Id) whenever a node's panelGuid is shared by another of the master's direct nodes. Applied consistently at originGuidFor (origin_node_guid write), the online-tree key plus a self-key remap for nodes that report a GUID-keyed tree, effectiveNodeGuid, and recountByGuid's inbound bucketing. sharedNodeGuids computes the collision set. Online now works without node changes; making panelGuids unique restores real-GUID identity and also fixes GUID-keyed IP attribution.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// Cloned node servers ship an identical panelGuid in their copied settings.
|
||||
// effectiveNodeGuid must keep each physical node in its own attribution bucket
|
||||
// by falling back to the node-unique id when a GUID is shared, while leaving a
|
||||
// uniquely-identified node on its real GUID.
|
||||
func TestEffectiveNodeGuid_DisambiguatesSharedGuids(t *testing.T) {
|
||||
nodes := []*model.Node{
|
||||
{Id: 1, Guid: "dup"},
|
||||
{Id: 2, Guid: "dup"},
|
||||
{Id: 3, Guid: "uniq"},
|
||||
{Id: 4, Guid: ""},
|
||||
{Id: 0, Guid: "transitive"},
|
||||
}
|
||||
shared := sharedNodeGuids(nodes)
|
||||
|
||||
if _, ok := shared["dup"]; !ok {
|
||||
t.Fatalf("dup must be flagged shared, got %v", shared)
|
||||
}
|
||||
if _, ok := shared["uniq"]; ok {
|
||||
t.Fatalf("uniq must not be shared, got %v", shared)
|
||||
}
|
||||
if _, ok := shared["transitive"]; ok {
|
||||
t.Fatalf("transitive (Id 0) must not count toward sharing, got %v", shared)
|
||||
}
|
||||
|
||||
cases := map[*model.Node]string{
|
||||
nodes[0]: "node:1",
|
||||
nodes[1]: "node:2",
|
||||
nodes[2]: "uniq",
|
||||
nodes[3]: "node:4",
|
||||
nodes[4]: "transitive",
|
||||
}
|
||||
for n, want := range cases {
|
||||
if got := effectiveNodeGuid(n, shared); got != want {
|
||||
t.Errorf("effectiveNodeGuid(Id=%d, Guid=%q) = %q, want %q", n.Id, n.Guid, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recountByGuid must split per-node counts even when two direct nodes share a
|
||||
// GUID and their inbounds still carry that shared GUID as origin (pre-backfill).
|
||||
func TestRecountByGuid_SplitsClonedNodesWithSharedGuid(t *testing.T) {
|
||||
setupConflictDB(t)
|
||||
db := database.GetDB()
|
||||
svc := NodeService{}
|
||||
selfGuid, _ := (&SettingService{}).GetPanelGuid()
|
||||
|
||||
n1 := &model.Node{Id: 1, Name: "A", Address: "10.0.0.1", Port: 2053, ApiToken: "t", Guid: "dup", Status: "online"}
|
||||
n2 := &model.Node{Id: 2, Name: "B", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "dup", Status: "online"}
|
||||
n3 := &model.Node{Id: 3, Name: "C", Address: "10.0.0.3", Port: 2053, ApiToken: "t", Guid: "uniq", Status: "online"}
|
||||
for _, n := range []*model.Node{n1, n2, n3} {
|
||||
if err := db.Create(n).Error; err != nil {
|
||||
t.Fatalf("create node %s: %v", n.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
id1, id2, id3 := 1, 2, 3
|
||||
inbounds := []*model.Inbound{
|
||||
{Tag: "a", Port: 1001, Protocol: model.VLESS, Settings: `{"clients":[]}`, Enable: true, NodeID: &id1, OriginNodeGuid: "dup"},
|
||||
{Tag: "b", Port: 1002, Protocol: model.VLESS, Settings: `{"clients":[]}`, Enable: true, NodeID: &id2, OriginNodeGuid: "dup"},
|
||||
{Tag: "c", Port: 1003, Protocol: model.VLESS, Settings: `{"clients":[]}`, Enable: true, NodeID: &id3, OriginNodeGuid: "uniq"},
|
||||
}
|
||||
for _, ib := range inbounds {
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("create inbound %s: %v", ib.Tag, err)
|
||||
}
|
||||
}
|
||||
|
||||
nodes := []*model.Node{n1, n2, n3}
|
||||
svc.recountByGuid(nodes, selfGuid)
|
||||
|
||||
if n1.InboundCount != 1 || n2.InboundCount != 1 {
|
||||
t.Errorf("cloned nodes must not share inbound counts: n1=%d n2=%d, want 1,1", n1.InboundCount, n2.InboundCount)
|
||||
}
|
||||
if n3.InboundCount != 1 {
|
||||
t.Errorf("unique node InboundCount = %d, want 1", n3.InboundCount)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user