mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-17 05:52:17 +09:00
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
This commit is contained in:
@@ -438,9 +438,9 @@ func (m *mapper) buildFromChange(
|
|||||||
// incremental peer-change and user-profile paths, computed from the same live
|
// incremental peer-change and user-profile paths, computed from the same live
|
||||||
// per-node matchers and [policy.ReduceNodes] filter that
|
// per-node matchers and [policy.ReduceNodes] filter that
|
||||||
// [MapResponseBuilder.buildTailPeers] applies to full peer objects, so the
|
// [MapResponseBuilder.buildTailPeers] applies to full peer objects, so the
|
||||||
// paths cannot drift. The snapshot peer map ([NodeStore.ListPeers]) is used
|
// paths cannot drift. The recipient's adjacency from the NodeStore
|
||||||
// only as the candidate set, matching buildTailPeers; the live policy decides
|
// ([NodeStore.ListPeers]) is the authority for which peers exist for it; the
|
||||||
// visibility because the snapshot is not rebuilt on policy changes.
|
// live matchers only narrow that set further, matching buildTailPeers.
|
||||||
//
|
//
|
||||||
// ok is false when the node or its matchers cannot be resolved; callers must
|
// ok is false when the node or its matchers cannot be resolved; callers must
|
||||||
// then fail closed (emit nothing) rather than risk leaking forbidden peers.
|
// then fail closed (emit nothing) rather than risk leaking forbidden peers.
|
||||||
|
|||||||
@@ -485,12 +485,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
|
||||||
@@ -505,7 +505,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:*"]}]}`,
|
||||||
|
|||||||
@@ -865,14 +865,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{}{}
|
||||||
@@ -880,20 +876,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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user