fix(nodes): extend duplicate-GUID hardening to master collisions, IP attribution, and a heartbeat warning

Builds on the node-vs-node fix: a node's GUID is now also treated as ambiguous when it equals the master's own panelGuid (a node cloned from the master), so the master's local clients and that node can't merge. Centralized as ambiguousNodeGuids(nodes, selfGuid) + effectiveNodeKey(node).

Applied the same node-unique fallback to the GUID-keyed IP attribution that #4983 added but the prior commit left collapsing: MergeClientIpsByGuid remaps a cloned node's own subtree to its node-unique key, nodeGuidNameMap resolves names by that key, and node deletion purges both keys. Added a throttled heartbeat warning so the operator is told to regenerate a duplicate panelGuid. Tests cover master-collision, effectiveNodeKey, and the IP remap.
This commit is contained in:
MHSanaei
2026-06-22 15:08:02 +02:00
parent af941798c6
commit 98c9ba1f91
7 changed files with 215 additions and 64 deletions
+7 -15
View File
@@ -211,23 +211,15 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
// 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. That
// key is the node's panelGuid, unless another of this master's nodes reports
// the same GUID (cloned server — the panelGuid ships in the copied settings),
// in which case it would collapse both nodes into one #4983 bucket, so fall
// back to the node-unique id. Old-build nodes with no GUID use the id too.
// 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.
var nodeRow model.Node
db.Select("guid").Where("id = ?", nodeID).First(&nodeRow)
guidShared := false
if nodeRow.Guid != "" {
var sameGuid int64
db.Model(&model.Node{}).Where("guid = ?", nodeRow.Guid).Count(&sameGuid)
guidShared = sameGuid > 1
}
selfKey := nodeRow.Guid
if selfKey == "" || guidShared {
selfKey = synthNodeGuid(nodeID)
}
selfKey := effectiveNodeKey(&model.Node{Id: nodeID, Guid: nodeRow.Guid})
guidShared := nodeRow.Guid != "" && selfKey != nodeRow.Guid
originGuidFor := func(snapIb *model.Inbound) string {
if snapIb.OriginNodeGuid != "" {
return snapIb.OriginNodeGuid