From 2db82fc302d15148726fbf25ae31b357fa93e668 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 4 Jun 2026 10:35:35 +0100 Subject: [PATCH] Reject Docker host match on conflicting physical identity (#1366) Back-port v5 fix 333e66a8e to v6, adapted to v6's DockerHostView accessors. findMatchingDockerHost now skips an agentID/token match when dockerHostIdentityConflicts detects a different MachineID, Swarm NodeID, or (absent stronger IDs) hostname, so two physically different Docker nodes that share a token+agentID are not collapsed into one host record. Adds a regression test. --- internal/monitoring/docker_host_identity.go | 43 +++++++++++++++++++ .../monitoring/docker_host_identity_test.go | 25 +++++++++++ 2 files changed, 68 insertions(+) diff --git a/internal/monitoring/docker_host_identity.go b/internal/monitoring/docker_host_identity.go index 3922f78d6..d903b4cf9 100644 --- a/internal/monitoring/docker_host_identity.go +++ b/internal/monitoring/docker_host_identity.go @@ -87,6 +87,9 @@ func findMatchingDockerHost(hosts []*unifiedresources.DockerHostView, report age existingToken := strings.TrimSpace(host.TokenID()) if tokenID == "" || existingToken == tokenID { + if dockerHostIdentityConflicts(host, report) { + continue + } return host, true } } @@ -147,6 +150,46 @@ func findMatchingDockerHost(hosts []*unifiedresources.DockerHostView, report age return nil, false } +// dockerHostIdentityConflicts reports whether an incoming report is clearly from +// a different physical/swarm node than the existing Docker host record, so a +// shared token+agentID reused from another node is not collapsed into one host. +func dockerHostIdentityConflicts(existing *unifiedresources.DockerHostView, report agentsdocker.Report) bool { + if existing == nil { + return false + } + + existingMachineID := strings.TrimSpace(existing.MachineID()) + reportMachineID := strings.TrimSpace(report.Host.MachineID) + if existingMachineID != "" && reportMachineID != "" && existingMachineID != reportMachineID { + return true + } + + existingSwarmNodeID := "" + if sw := existing.Swarm(); sw != nil { + existingSwarmNodeID = strings.TrimSpace(sw.NodeID) + } + reportSwarmNodeID := "" + if report.Host.Swarm != nil { + reportSwarmNodeID = strings.TrimSpace(report.Host.Swarm.NodeID) + } + if existingSwarmNodeID != "" && reportSwarmNodeID != "" && existingSwarmNodeID != reportSwarmNodeID { + return true + } + + // Without stronger stable IDs, a hostname change is ambiguous and should not + // be treated as the same reporting host automatically. + existingHostname := strings.TrimSpace(existing.Hostname()) + reportHostname := strings.TrimSpace(report.Host.Hostname) + if existingMachineID == "" && reportMachineID == "" && + existingSwarmNodeID == "" && reportSwarmNodeID == "" && + existingHostname != "" && reportHostname != "" && + !unifiedresources.HostnamesEquivalent(existingHostname, reportHostname) { + return true + } + + return false +} + func dockerHostStableID(host *unifiedresources.DockerHostView) string { if host == nil { return "" diff --git a/internal/monitoring/docker_host_identity_test.go b/internal/monitoring/docker_host_identity_test.go index c808a3b73..301ec8627 100644 --- a/internal/monitoring/docker_host_identity_test.go +++ b/internal/monitoring/docker_host_identity_test.go @@ -952,3 +952,28 @@ func TestSanitizeDockerHostSuffix_UnicodeRunes(t *testing.T) { t.Errorf("got %q, want %q", result, expected) } } + +func TestFindMatchingDockerHost_RejectsConflictingPhysicalIdentity(t *testing.T) { + t.Parallel() + + hosts := []models.DockerHost{ + { + ID: "host1", + AgentID: "agent-1", + TokenID: "token-1", + MachineID: "machine-1", + Hostname: "hostname-1", + }, + } + + // Same agentID + token, but the report comes from a physically different + // machine — it must NOT be collapsed into the existing host (#1366). + report := agentsdocker.Report{ + Agent: agentsdocker.AgentInfo{ID: "agent-1"}, + Host: agentsdocker.HostInfo{MachineID: "machine-2", Hostname: "hostname-2"}, + } + + if _, ok := findMatchingDockerHost(dockerHostViewsForTest(hosts), report, &config.APITokenRecord{ID: "token-1"}); ok { + t.Fatal("expected no match when the report's physical identity conflicts with the existing host") + } +}