Ver código fonte

fix(nodes): remap a cloned node's own-panelGuid origin so the inbound page shows online

These nodes report their OWN inbounds with their own panelGuid as OriginNodeGuid, so originGuidFor returned the shared GUID verbatim and never remapped it. origin_node_guid stayed the shared GUID while online was keyed under the node-unique key, so the inbound page (which reads the stored origin_node_guid) looked up an empty bucket and showed everyone offline — even though the Nodes page (which derives the key live) was correct. Treat an origin equal to the node's own panelGuid as the node's own inbound and resolve it through selfKey; keep only a genuinely different (descendant) origin across hops.
MHSanaei 8 horas atrás
pai
commit
7458ed4064

+ 9 - 8
internal/web/service/inbound_node.go

@@ -209,19 +209,20 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
 	now := time.Now().UnixMilli()
 
 	// originGuidFor attributes a synced inbound to the panel that physically
-	// hosts it: inbounds the node forwards from its own sub-nodes already carry
-	// a non-empty OriginNodeGuid (kept as-is across hops); the node's own local
-	// inbounds report empty, so they are attributed to the node's own key —
-	// effectiveNodeKey, which is the node's panelGuid unless that GUID is
-	// ambiguous (shared with another node or the master, i.e. a cloned server),
-	// in which case it falls back to the node-unique id so #4983 attribution
-	// doesn't collapse two physical nodes into one bucket.
+	// hosts it. A node's OWN inbounds report either an empty origin or — on
+	// builds that set it locally — the node's own panelGuid; both resolve to
+	// selfKey, which is the node's panelGuid unless that GUID is ambiguous
+	// (shared with another node or the master, i.e. a cloned server), in which
+	// case it falls back to the node-unique id so #4983 attribution doesn't
+	// collapse two physical nodes into one bucket. Only a DIFFERENT, non-empty
+	// origin (an inbound the node forwards from its own sub-node) is kept as-is,
+	// so a chained Node1->Node2->Node3 still attributes Node3's inbounds to Node3.
 	var nodeRow model.Node
 	db.Select("guid").Where("id = ?", nodeID).First(&nodeRow)
 	selfKey := effectiveNodeKey(&model.Node{Id: nodeID, Guid: nodeRow.Guid})
 	guidShared := nodeRow.Guid != "" && selfKey != nodeRow.Guid
 	originGuidFor := func(snapIb *model.Inbound) string {
-		if snapIb.OriginNodeGuid != "" {
+		if snapIb.OriginNodeGuid != "" && snapIb.OriginNodeGuid != nodeRow.Guid {
 			return snapIb.OriginNodeGuid
 		}
 		return selfKey

+ 61 - 0
internal/web/service/node_origin_guid_test.go

@@ -70,6 +70,67 @@ func TestSetRemoteTraffic_AttributesOriginNodeGuid(t *testing.T) {
 	}
 }
 
+// A cloned node reports its OWN inbound with its own (duplicated) panelGuid as
+// the origin. That must be remapped to the node-unique key, not stored verbatim
+// — otherwise origin_node_guid keeps the shared GUID while online is keyed by
+// the node-unique key, and the inbound page reads an empty bucket (shows
+// offline). A genuinely forwarded sub-node GUID is still kept across the hop.
+func TestSetRemoteTraffic_RemapsClonedNodeOwnGuidOrigin(t *testing.T) {
+	setupConflictDB(t)
+	db := database.GetDB()
+
+	// Two nodes share one panelGuid (cloned servers).
+	for _, n := range []*model.Node{
+		{Id: 1, Name: "a", Address: "10.0.0.1", Port: 2053, ApiToken: "t", Guid: "dup"},
+		{Id: 2, Name: "b", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "dup"},
+	} {
+		if err := db.Create(n).Error; err != nil {
+			t.Fatalf("create node %s: %v", n.Name, err)
+		}
+	}
+
+	snap := &runtime.TrafficSnapshot{
+		Inbounds: []*model.Inbound{
+			{ // node 1's OWN inbound, reporting its own (shared) panelGuid as origin
+				Tag:            "own-443-tcp",
+				Enable:         true,
+				Port:           443,
+				Protocol:       model.VLESS,
+				Settings:       `{"clients":[]}`,
+				OriginNodeGuid: "dup",
+			},
+			{ // forwarded from a sub-node with a distinct guid — kept across the hop
+				Tag:            "fwd-8443-tcp",
+				Enable:         true,
+				Port:           8443,
+				Protocol:       model.VLESS,
+				Settings:       `{"clients":[]}`,
+				OriginNodeGuid: "child-guid",
+			},
+		},
+	}
+
+	svc := InboundService{}
+	if _, err := svc.setRemoteTrafficLocked(1, snap, false); err != nil {
+		t.Fatalf("setRemoteTrafficLocked: %v", err)
+	}
+
+	origin := func(tag string) string {
+		var ib model.Inbound
+		if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
+			t.Fatalf("load inbound %q: %v", tag, err)
+		}
+		return ib.OriginNodeGuid
+	}
+
+	if og := origin("own-443-tcp"); og != "node:1" {
+		t.Fatalf("cloned node's own inbound origin = %q, want node:1 (remapped from shared GUID)", og)
+	}
+	if og := origin("fwd-8443-tcp"); og != "child-guid" {
+		t.Fatalf("forwarded inbound origin = %q, want child-guid (kept across the hop)", og)
+	}
+}
+
 func TestSetRemoteTraffic_PreservesLocalShareAddressStrategy(t *testing.T) {
 	setupConflictDB(t)
 	db := database.GetDB()