mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Merge candidate 20260906T081300Z-core-runtime
Change-source: pulse-maintainer
This commit is contained in:
@@ -15,6 +15,24 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
### Automatic PVE association identity boundary
|
||||
|
||||
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.
|
||||
|
||||
Verification: `TestApplyHostReportDoesNotLinkUnrelatedDockerBridge` in
|
||||
`internal/monitoring/monitor_host_agents_test.go` ingests repeated synthetic
|
||||
NAS reports and requires both association directions to remain absent.
|
||||
The adjacent matcher and filtering tests exercise host-local rejection and
|
||||
retained management-address evidence. These are local fixture proofs, not
|
||||
reporter confirmation of #1930 or installed lifecycle qualification.
|
||||
|
||||
The internal Patrol request bridge carries explicit execution limits and
|
||||
capability allowlists without a diagnostic report-count budget. Finding writes
|
||||
retain their server-owned scope and cannot enter or satisfy the infrastructure
|
||||
|
||||
@@ -17,6 +17,27 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
### 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.
|
||||
|
||||
Verification in `internal/monitoring/monitor_host_agents_test.go`:
|
||||
`TestFindLinkedProxmoxEntityWithHints_RejectsHostLocalNetworkIdentity` covers
|
||||
seven previously false associations; `TestAgentLinkNetworkIdentityFiltering`
|
||||
pins both-side exclusions and positive management controls;
|
||||
`TestApplyHostReportDoesNotLinkUnrelatedDockerBridge` requires repeated
|
||||
ingestion to leave both link directions absent. Existing endpoint and
|
||||
ambiguous-name tests remain required. These checks prevent a reproduced
|
||||
synthetic misassociation, not all private-address collisions or unknown
|
||||
custom bridge names, and do not prove the cause of #1930.
|
||||
|
||||
Physical disk inventory has an independent collector schedule. The PVE poller
|
||||
carries its default five-minute or configured interval with each disk record,
|
||||
while keeping the last successful observation timestamp on retained records.
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -4389,18 +4390,38 @@ func (m *Monitor) findLinkedProxmoxEntity(hostname string) (nodeID, vmID, contai
|
||||
return m.findLinkedProxmoxEntityWithHints(hostname, "", nil)
|
||||
}
|
||||
|
||||
// Host-local addresses may be unique among the monitored PVE nodes while also
|
||||
// existing on an unrelated agent host. They are not machine identity evidence.
|
||||
func normalizeAgentLinkIP(address string) string {
|
||||
normalized := unifiedresources.NormalizeIP(address)
|
||||
if ip := net.ParseIP(normalized); ip != nil && ip.IsGlobalUnicast() {
|
||||
return normalized
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Do not discard all bridges: Proxmox management addresses commonly use vmbr0.
|
||||
// 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-")
|
||||
}
|
||||
|
||||
func collectReportedHostIPs(
|
||||
reportIP string,
|
||||
network []agentshost.NetworkInterface,
|
||||
) map[string]struct{} {
|
||||
ips := make(map[string]struct{})
|
||||
if normalized := unifiedresources.NormalizeIP(reportIP); normalized != "" {
|
||||
if normalized := normalizeAgentLinkIP(reportIP); normalized != "" {
|
||||
ips[normalized] = struct{}{}
|
||||
}
|
||||
|
||||
for _, nic := range network {
|
||||
if isHostLocalAgentLinkInterface(nic.Name) {
|
||||
continue
|
||||
}
|
||||
for _, address := range nic.Addresses {
|
||||
if normalized := unifiedresources.NormalizeIP(address); normalized != "" {
|
||||
if normalized := normalizeAgentLinkIP(address); normalized != "" {
|
||||
ips[normalized] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -4412,8 +4433,11 @@ func collectReportedHostIPs(
|
||||
func collectNodeNetworkIPs(network []unifiedresources.NetworkInterface) map[string]struct{} {
|
||||
ips := make(map[string]struct{})
|
||||
for _, nic := range network {
|
||||
if isHostLocalAgentLinkInterface(nic.Name) {
|
||||
continue
|
||||
}
|
||||
for _, address := range nic.Addresses {
|
||||
if normalized := unifiedresources.NormalizeIP(address); normalized != "" {
|
||||
if normalized := normalizeAgentLinkIP(address); normalized != "" {
|
||||
ips[normalized] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5685,3 +5685,100 @@ func TestApplyHostReportPreservesPoolOnlyUnraidCount(t *testing.T) {
|
||||
t.Fatalf("pool-only host raised storage risk: %+v", assessment)
|
||||
}
|
||||
}
|
||||
|
||||
// Uniqueness among PVE nodes does not make host-local addresses machine identity:
|
||||
// a non-PVE host is not included in that ownership count.
|
||||
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"},
|
||||
{"loopback", "lo", "127.0.0.1/8"},
|
||||
{"IPv6 loopback", "lo", "::1/128"},
|
||||
{"link local", "eth0", "169.254.1.2/16"},
|
||||
{"IPv6 link local", "eth0", "fe80::1/64"},
|
||||
{"unspecified", "eth0", "0.0.0.0"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
monitor := &Monitor{state: models.NewState()}
|
||||
monitor.state.UpdateNodes([]models.Node{{
|
||||
ID: "pve-node", Name: "pve", Instance: "cluster",
|
||||
Host: "https://pve.example:8006",
|
||||
NetworkInterfaces: []models.HostNetworkInterface{{Name: tc.nic, Addresses: []string{tc.address}}},
|
||||
}})
|
||||
node, vm, ct := monitor.findLinkedProxmoxEntityWithHints("nas", "", []agentshost.NetworkInterface{
|
||||
{Name: tc.nic, Addresses: []string{tc.address}},
|
||||
{Name: "eth1", Addresses: []string{"198.51.100.20/24"}},
|
||||
})
|
||||
if node != "" || vm != "" || ct != "" {
|
||||
t.Fatalf("unrelated NAS linked by host-local address: node=%q vm=%q ct=%q", node, vm, ct)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentLinkNetworkIdentityFiltering(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, nic, address string
|
||||
want bool
|
||||
}{
|
||||
{"management bridge", "vmbr0", "192.0.2.10/24", true},
|
||||
{"private management", "eth0", "172.17.0.1/16", true},
|
||||
{"IPv6 management", "vmbr0", "2001:db8::10/64", true},
|
||||
{"ULA management", "eth0", "fd00::10/64", true},
|
||||
{"unnamed legacy interface", "", "192.0.2.10", true},
|
||||
{"docker", "docker0", "172.17.0.1/16", false},
|
||||
{"custom docker", "br-abc", "192.0.2.1/24", false},
|
||||
{"loopback", "eth0", "127.1.2.3/8", false},
|
||||
{"IPv6 unspecified", "eth0", "::", false},
|
||||
{"multicast", "eth0", "224.0.0.1", false},
|
||||
{"IPv6 link local", "eth0", "fe90::1/64", false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
agent := collectReportedHostIPs("", []agentshost.NetworkInterface{{Name: tc.nic, Addresses: []string{tc.address}}})
|
||||
node := collectNodeNetworkIPs([]unifiedresources.NetworkInterface{{Name: tc.nic, Addresses: []string{tc.address}}})
|
||||
if (len(agent) == 1) != tc.want || (len(node) == 1) != tc.want {
|
||||
t.Fatalf("agent=%v node=%v want accepted=%v", agent, node, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if got := collectReportedHostIPs("172.17.0.1", nil); len(got) != 1 {
|
||||
t.Fatalf("explicit private report IP must remain usable: %v", got)
|
||||
}
|
||||
if got := collectReportedHostIPs("::1", nil); len(got) != 0 {
|
||||
t.Fatalf("explicit loopback cannot identify a machine: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Exercise ingestion as well as the matcher: no reciprocal PVE link may be
|
||||
// created from a bridge address shared with an unrelated host.
|
||||
func TestApplyHostReportDoesNotLinkUnrelatedDockerBridge(t *testing.T) {
|
||||
monitor := issue1654Monitor()
|
||||
monitor.state.UpdateNodes([]models.Node{{
|
||||
ID: "pve-node", Name: "pve", Instance: "cluster",
|
||||
Host: "https://pve.example:8006",
|
||||
NetworkInterfaces: []models.HostNetworkInterface{
|
||||
{Name: "vmbr0", Addresses: []string{"192.0.2.10/24"}},
|
||||
{Name: "docker0", Addresses: []string{"172.17.0.1/16"}},
|
||||
},
|
||||
}})
|
||||
report := issue1654Report(time.Now().UTC())
|
||||
report.Host.Hostname = "nas.example"
|
||||
report.Network = []agentshost.NetworkInterface{
|
||||
{Name: "eth0", Addresses: []string{"198.51.100.20/24"}},
|
||||
{Name: "docker0", Addresses: []string{"172.17.0.1/16"}},
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
report.Timestamp = report.Timestamp.Add(time.Second)
|
||||
host, err := monitor.ApplyHostReport(report, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if host.LinkedNodeID != "" {
|
||||
t.Fatalf("NAS linked to %q", host.LinkedNodeID)
|
||||
}
|
||||
nodes := monitor.state.GetSnapshot().Nodes
|
||||
if len(nodes) != 1 || nodes[0].LinkedAgentID != "" {
|
||||
t.Fatalf("PVE node acquired an unrelated agent link: %+v", nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user