diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 4a0ad216c..91bdad6fe 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -21,10 +21,12 @@ Host ingestion must not create a host-to-PVE or reciprocal PVE-to-agent link solely from a shared host-local network address. Uniqueness among monitored PVE nodes does not establish uniqueness across unrelated agent hosts. Automatic network evidence excludes loopback, unspecified, multicast and -link-local IPs, and interfaces named lo or prefixed docker/br-. This changes -association evidence only: enrollment, token binding, removal, re-enrollment -and command authority remain unchanged. It does not migrate or repair -persisted incorrect links. +link-local IPs, interfaces named lo or prefixed docker, and Docker-generated +bridge names matching `br-` plus a 12-character hexadecimal network ID. Other +bridge names remain eligible because they may carry management traffic. This +changes association evidence only: enrollment, token binding, removal, +re-enrollment and command authority remain unchanged. It does not migrate or +repair persisted incorrect links. Verification: `TestApplyHostReportDoesNotLinkUnrelatedDockerBridge` in `internal/monitoring/monitor_host_agents_test.go` ingests repeated synthetic diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 6f5303afe..30cf3baaf 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -19,14 +19,15 @@ ### Host-local addresses are not PVE identity -Automatic host/PVE network matching excludes non-global-unicast addresses -and lo/docker*/br-* interfaces from both reported-host and provider-node -address inventories. A Docker bridge address seen on only one PVE node can -still exist on an unrelated NAS; provider-only owner counts cannot make it -machine identity. Management bridges such as vmbr0, unnamed legacy interfaces, -private IPv4 and IPv6/ULA remain eligible. Explicit unicast report-IP hints -remain eligible independently of inferred interface evidence. No blanket -private-subnet exclusion is permitted. +Automatic host/PVE network matching excludes non-global-unicast addresses, +lo/docker* interfaces and Docker-generated `br-` plus 12-hex-character bridge +names from both reported-host and provider-node address inventories. A Docker +bridge address seen on only one PVE node can still exist on an unrelated NAS; +provider-only owner counts cannot make it machine identity. Management bridges +such as vmbr0 and br-mgmt, unnamed legacy interfaces, private IPv4 and IPv6/ULA +remain eligible. Explicit unicast report-IP hints remain eligible independently +of inferred interface evidence. No blanket private-subnet exclusion is +permitted. Verification in `internal/monitoring/monitor_host_agents_test.go`: `TestFindLinkedProxmoxEntityWithHints_RejectsHostLocalNetworkIdentity` covers diff --git a/internal/monitoring/monitor_agents.go b/internal/monitoring/monitor_agents.go index 9b6129c80..4465f8ce7 100644 --- a/internal/monitoring/monitor_agents.go +++ b/internal/monitoring/monitor_agents.go @@ -4400,11 +4400,27 @@ func normalizeAgentLinkIP(address string) string { return "" } -// Do not discard all bridges: Proxmox management addresses commonly use vmbr0. -// Explicit report-IP and endpoint hints remain usable for routable addresses. +// Do not discard all bridges: Proxmox management addresses commonly use vmbr0, +// and operators may use names such as br-mgmt. Docker's automatically named +// user-defined bridges use br- followed by the first 12 hex characters of the +// network ID. Explicit report-IP and endpoint hints remain usable for routable +// addresses. func isHostLocalAgentLinkInterface(name string) bool { name = strings.ToLower(strings.TrimSpace(name)) - return name == "lo" || strings.HasPrefix(name, "docker") || strings.HasPrefix(name, "br-") + if name == "lo" || strings.HasPrefix(name, "docker") { + return true + } + + bridgeID, found := strings.CutPrefix(name, "br-") + if !found || len(bridgeID) != 12 { + return false + } + for _, character := range bridgeID { + if (character < '0' || character > '9') && (character < 'a' || character > 'f') { + return false + } + } + return true } func collectReportedHostIPs( diff --git a/internal/monitoring/monitor_host_agents_test.go b/internal/monitoring/monitor_host_agents_test.go index c5c4b363b..07c5246b7 100644 --- a/internal/monitoring/monitor_host_agents_test.go +++ b/internal/monitoring/monitor_host_agents_test.go @@ -5691,7 +5691,7 @@ func TestApplyHostReportPreservesPoolOnlyUnraidCount(t *testing.T) { func TestFindLinkedProxmoxEntityWithHints_RejectsHostLocalNetworkIdentity(t *testing.T) { for _, tc := range []struct{ name, nic, address string }{ {"docker", "docker0", "172.17.0.1/16"}, - {"custom docker bridge", "br-123abc", "192.0.2.1/24"}, + {"generated docker bridge", "br-0123456789ab", "192.0.2.1/24"}, {"loopback", "lo", "127.0.0.1/8"}, {"IPv6 loopback", "lo", "::1/128"}, {"link local", "eth0", "169.254.1.2/16"}, @@ -5726,8 +5726,10 @@ func TestAgentLinkNetworkIdentityFiltering(t *testing.T) { {"IPv6 management", "vmbr0", "2001:db8::10/64", true}, {"ULA management", "eth0", "fd00::10/64", true}, {"unnamed legacy interface", "", "192.0.2.10", true}, + {"custom management bridge", "br-mgmt", "192.0.2.10", true}, {"docker", "docker0", "172.17.0.1/16", false}, - {"custom docker", "br-abc", "192.0.2.1/24", false}, + {"generated docker", "br-0123456789ab", "192.0.2.1/24", false}, + {"non-hex bridge", "br-0123456789ag", "192.0.2.1/24", true}, {"loopback", "eth0", "127.1.2.3/8", false}, {"IPv6 unspecified", "eth0", "::", false}, {"multicast", "eth0", "224.0.0.1", false},