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.
This commit is contained in:
rcourtman 2026-06-04 10:35:35 +01:00
parent a6b773a85f
commit 2db82fc302
2 changed files with 68 additions and 0 deletions

View file

@ -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 ""

View file

@ -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")
}
}