state: resolve changed peers through adjacency

ListPeers now filters named peers against the recipient's adjacency, so
a node the policy hides is never delivered.

Updates #3417

(cherry picked from commit 4c6a2dff52)
This commit is contained in:
Kristoffer Dalby
2026-09-09 14:43:35 +00:00
parent 237dc74e73
commit 6a0f67d56e
3 changed files with 17 additions and 34 deletions
+7 -7
View File
@@ -484,12 +484,12 @@ func TestBuildFromChangeVisibilityMatchesFullMap(t *testing.T) {
return false return false
} }
// wantFull pins the actual peer-visibility semantics so the invariant below // wantFull pins the actual peer-visibility semantics so the cross-path
// cannot pass vacuously (e.g. if every path broke to zero identically). // check below cannot pass vacuously (e.g. if every path broke to zero
// Note deny_all: an empty ACL set compiles to zero matchers, which headscale // identically).
// treats as "no visibility restriction" — all peers are visible on every // Note deny_all: an empty ACL set yields no peer adjacency, so nothing is
// path (the packet filter denies traffic separately). user_isolation and // visible on any path. user_isolation and autogroup_self remain the
// autogroup_self are the discriminating cases that prove filtering works. // discriminating cases that prove filtering works.
tests := []struct { tests := []struct {
name string name string
policy string policy string
@@ -504,7 +504,7 @@ func TestBuildFromChangeVisibilityMatchesFullMap(t *testing.T) {
]}`, ]}`,
1, 1,
}, },
{"deny_all", `{"acls":[]}`, 2}, {"deny_all", `{"acls":[]}`, 0},
{ {
"autogroup_self", "autogroup_self",
`{"acls":[{"action":"accept","src":["autogroup:member"],"dst":["autogroup:self:*"]}]}`, `{"acls":[{"action":"accept","src":["autogroup:member"],"dst":["autogroup:self:*"]}]}`,
+1 -5
View File
@@ -284,11 +284,7 @@ func TestBuildPeerMapFromPolicy(t *testing.T) {
want = append(want, n.ID) want = append(want, n.ID)
} }
var got []types.NodeID got := pm.BuildPeerMap(tt.nodes.ViewSlice())[tt.node.ID]
for _, n := range pm.BuildPeerMap(tt.nodes.ViewSlice())[tt.node.ID] {
got = append(got, n.ID())
}
if !assert.ElementsMatch(t, want, got) { if !assert.ElementsMatch(t, want, got) {
t.Log("Matchers: ") t.Log("Matchers: ")
+9 -22
View File
@@ -871,14 +871,10 @@ func (s *State) ListPeers(nodeID types.NodeID, peerIDs ...types.NodeID) views.Sl
return s.nodeStore.ListPeers(nodeID) return s.nodeStore.ListPeers(nodeID)
} }
// For specific peerIDs, filter from all nodes. // Incremental updates (NodeAdded, NodeChanged) name the peers involved.
// This path is used for incremental updates (NodeAdded, NodeChanged) // Resolve them through the recipient's adjacency so a changed node the
// where the caller already knows which peer IDs are involved. // policy hides from this recipient is never delivered; the mapper still
// Peer visibility filtering happens in the mapper against the live // applies the live matchers on top.
// policy (buildTailPeers and the shared visiblePeerIDs filter), because
// the snapshot peer map is not rebuilt on policy changes.
allNodes := s.nodeStore.ListNodes()
nodeIDSet := make(map[types.NodeID]struct{}, len(peerIDs)) nodeIDSet := make(map[types.NodeID]struct{}, len(peerIDs))
for _, id := range peerIDs { for _, id := range peerIDs {
nodeIDSet[id] = struct{}{} nodeIDSet[id] = struct{}{}
@@ -886,20 +882,11 @@ func (s *State) ListPeers(nodeID types.NodeID, peerIDs ...types.NodeID) views.Sl
var filteredNodes []types.NodeView var filteredNodes []types.NodeView
for _, node := range allNodes.All() { // Adjacency is built from node pairs, so it never contains the
// A node is never its own peer. [db.ListPeers] enforces this with // recipient: a change batch naming it cannot return it as its own peer.
// `id <> nodeID`; the caller may name the recipient in peerIDs for _, peer := range s.nodeStore.ListPeers(nodeID).All() {
// (a change batch that includes it), and the mapper's only other if _, exists := nodeIDSet[peer.ID()]; exists {
// self filter is [policy.ReduceNodes], which is skipped when the filteredNodes = append(filteredNodes, peer)
// node has no matchers. Self would then reach the client in
// [tailcfg.MapResponse.PeersChanged], where it is merged into the
// peer map and listed alongside the self node.
if node.ID() == nodeID {
continue
}
if _, exists := nodeIDSet[node.ID()]; exists {
filteredNodes = append(filteredNodes, node)
} }
} }