mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-06 06:40:06 +09:00
policy,state: key the peer map by node ID
Adjacency becomes immutable, so a snapshot can resolve peers through its
own fresh views instead of storing them.
Updates #3417
(cherry picked from commit 95ba787417)
This commit is contained in:
@@ -19,7 +19,7 @@ type PolicyManager interface {
|
|||||||
// MatchersForNode returns matchers for peer relationship determination (unreduced)
|
// MatchersForNode returns matchers for peer relationship determination (unreduced)
|
||||||
MatchersForNode(node types.NodeView) ([]matcher.Match, error)
|
MatchersForNode(node types.NodeView) ([]matcher.Match, error)
|
||||||
// BuildPeerMap constructs peer relationship maps for the given nodes
|
// BuildPeerMap constructs peer relationship maps for the given nodes
|
||||||
BuildPeerMap(nodes views.Slice[types.NodeView]) map[types.NodeID][]types.NodeView
|
BuildPeerMap(nodes views.Slice[types.NodeView]) map[types.NodeID][]types.NodeID
|
||||||
SSHPolicy(baseURL string, node types.NodeView) (*tailcfg.SSHPolicy, error)
|
SSHPolicy(baseURL string, node types.NodeView) (*tailcfg.SSHPolicy, error)
|
||||||
// SSHCheckParams resolves the SSH check period for a (src, dst) pair
|
// SSHCheckParams resolves the SSH check period for a (src, dst) pair
|
||||||
// from the current policy, avoiding trust of client-provided URL params.
|
// from the current policy, avoiding trust of client-provided URL params.
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ func TestIssue3212AutogroupInternetExitVisibility(t *testing.T) {
|
|||||||
map[types.NodeID]struct{},
|
map[types.NodeID]struct{},
|
||||||
len(peerMap[srcNode.ID]),
|
len(peerMap[srcNode.ID]),
|
||||||
)
|
)
|
||||||
for _, p := range peerMap[srcNode.ID] {
|
for _, id := range peerMap[srcNode.ID] {
|
||||||
peerIDs[p.ID()] = struct{}{}
|
peerIDs[id] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, exitName := range exitNames {
|
for _, exitName := range exitNames {
|
||||||
|
|||||||
@@ -72,15 +72,11 @@ func TestIssue3233ViaInternetExitVisibility(t *testing.T) {
|
|||||||
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
||||||
|
|
||||||
require.True(t,
|
require.True(t,
|
||||||
slices.ContainsFunc(peerMap[aliceLaptop.ID], func(n types.NodeView) bool {
|
slices.Contains(peerMap[aliceLaptop.ID], aliceExit.ID),
|
||||||
return n.ID() == aliceExit.ID
|
|
||||||
}),
|
|
||||||
"alice must see her tag:exit1 exit node as a peer")
|
"alice must see her tag:exit1 exit node as a peer")
|
||||||
|
|
||||||
require.False(t,
|
require.False(t,
|
||||||
slices.ContainsFunc(peerMap[aliceLaptop.ID], func(n types.NodeView) bool {
|
slices.Contains(peerMap[aliceLaptop.ID], bobExit.ID),
|
||||||
return n.ID() == bobExit.ID
|
|
||||||
}),
|
|
||||||
"alice must not see bob's tag:exit2 exit node — via grant scopes to tag:exit1")
|
"alice must not see bob's tag:exit2 exit node — via grant scopes to tag:exit1")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ func (pm *PolicyManager) Filter() ([]tailcfg.FilterRule, []matcher.Match) {
|
|||||||
// up with doing the full work for every node O(n^2), while this will reduce
|
// up with doing the full work for every node O(n^2), while this will reduce
|
||||||
// the list as we see relationships while building the map, making it
|
// the list as we see relationships while building the map, making it
|
||||||
// O(n^2/2) in the end, but with less work per node.
|
// O(n^2/2) in the end, but with less work per node.
|
||||||
func (pm *PolicyManager) BuildPeerMap(nodes views.Slice[types.NodeView]) map[types.NodeID][]types.NodeView {
|
func (pm *PolicyManager) BuildPeerMap(nodes views.Slice[types.NodeView]) map[types.NodeID][]types.NodeID {
|
||||||
if pm == nil {
|
if pm == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -627,7 +627,7 @@ func (pm *PolicyManager) BuildPeerMap(nodes views.Slice[types.NodeView]) map[typ
|
|||||||
// Via grants require the per-node path because the global filter
|
// Via grants require the per-node path because the global filter
|
||||||
// skips via grants (compileFilterRules: if len(grant.Via) > 0 { continue }).
|
// skips via grants (compileFilterRules: if len(grant.Via) > 0 { continue }).
|
||||||
if !pm.needsPerNodeFilter {
|
if !pm.needsPerNodeFilter {
|
||||||
ret := make(map[types.NodeID][]types.NodeView, nodes.Len())
|
ret := make(map[types.NodeID][]types.NodeID, nodes.Len())
|
||||||
|
|
||||||
// Build the map of all peers according to the matchers.
|
// Build the map of all peers according to the matchers.
|
||||||
for i := range nodes.Len() {
|
for i := range nodes.Len() {
|
||||||
@@ -639,8 +639,8 @@ func (pm *PolicyManager) BuildPeerMap(nodes views.Slice[types.NodeView]) map[typ
|
|||||||
ri, rj := routeInfo[nodes.At(i).ID()], routeInfo[nodes.At(j).ID()]
|
ri, rj := routeInfo[nodes.At(i).ID()], routeInfo[nodes.At(j).ID()]
|
||||||
if nodes.At(i).CanAccessWithRoutes(pm.matchers, nodes.At(j), ri.subnet, rj.subnet, rj.isExit) ||
|
if nodes.At(i).CanAccessWithRoutes(pm.matchers, nodes.At(j), ri.subnet, rj.subnet, rj.isExit) ||
|
||||||
nodes.At(j).CanAccessWithRoutes(pm.matchers, nodes.At(i), rj.subnet, ri.subnet, ri.isExit) {
|
nodes.At(j).CanAccessWithRoutes(pm.matchers, nodes.At(i), rj.subnet, ri.subnet, ri.isExit) {
|
||||||
ret[nodes.At(i).ID()] = append(ret[nodes.At(i).ID()], nodes.At(j))
|
ret[nodes.At(i).ID()] = append(ret[nodes.At(i).ID()], nodes.At(j).ID())
|
||||||
ret[nodes.At(j).ID()] = append(ret[nodes.At(j).ID()], nodes.At(i))
|
ret[nodes.At(j).ID()] = append(ret[nodes.At(j).ID()], nodes.At(i).ID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,7 +649,7 @@ func (pm *PolicyManager) BuildPeerMap(nodes views.Slice[types.NodeView]) map[typ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For autogroup:self or via grants, build per-node peer relationships
|
// For autogroup:self or via grants, build per-node peer relationships
|
||||||
ret := make(map[types.NodeID][]types.NodeView, nodes.Len())
|
ret := make(map[types.NodeID][]types.NodeID, nodes.Len())
|
||||||
|
|
||||||
// Pre-compute per-node matchers using unreduced compiled rules
|
// Pre-compute per-node matchers using unreduced compiled rules
|
||||||
// We need unreduced rules to determine peer relationships correctly.
|
// We need unreduced rules to determine peer relationships correctly.
|
||||||
@@ -692,8 +692,8 @@ func (pm *PolicyManager) BuildPeerMap(nodes views.Slice[types.NodeView]) map[typ
|
|||||||
canIReachJ := hasFilterJ && nodeI.CanAccessWithRoutes(matchersJ, nodeJ, riI.subnet, riJ.subnet, riJ.isExit)
|
canIReachJ := hasFilterJ && nodeI.CanAccessWithRoutes(matchersJ, nodeJ, riI.subnet, riJ.subnet, riJ.isExit)
|
||||||
|
|
||||||
if canIAccessJ || canJAccessI || canJReachI || canIReachJ {
|
if canIAccessJ || canJAccessI || canJReachI || canIReachJ {
|
||||||
ret[nodeI.ID()] = append(ret[nodeI.ID()], nodeJ)
|
ret[nodeI.ID()] = append(ret[nodeI.ID()], nodeJ.ID())
|
||||||
ret[nodeJ.ID()] = append(ret[nodeJ.ID()], nodeI)
|
ret[nodeJ.ID()] = append(ret[nodeJ.ID()], nodeI.ID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -742,9 +742,8 @@ func TestAutogroupSelfWithOtherRules(t *testing.T) {
|
|||||||
test1Peers := peerMap[test1Node.ID]
|
test1Peers := peerMap[test1Node.ID]
|
||||||
|
|
||||||
// Verify test-1 can see the router (group:home -> tag:node-router rule)
|
// Verify test-1 can see the router (group:home -> tag:node-router rule)
|
||||||
require.True(t, slices.ContainsFunc(test1Peers, func(n types.NodeView) bool {
|
require.True(t, slices.Contains(test1Peers, test2RouterNode.ID),
|
||||||
return n.ID() == test2RouterNode.ID
|
"test-1 should see test-2's router via group:home -> tag:node-router rule, even when autogroup:self rule exists (issue #2838)")
|
||||||
}), "test-1 should see test-2's router via group:home -> tag:node-router rule, even when autogroup:self rule exists (issue #2838)")
|
|
||||||
|
|
||||||
// Verify that test-1 has filter rules (including autogroup:self and tag:node-router access)
|
// Verify that test-1 has filter rules (including autogroup:self and tag:node-router access)
|
||||||
rules, err := pm.FilterForNode(test1Node.View())
|
rules, err := pm.FilterForNode(test1Node.View())
|
||||||
@@ -904,12 +903,12 @@ func TestTagPropagationToPeerMap(t *testing.T) {
|
|||||||
// Check user2's peers - should include user1
|
// Check user2's peers - should include user1
|
||||||
user2Peers := initialPeerMap[user2Node.ID]
|
user2Peers := initialPeerMap[user2Node.ID]
|
||||||
require.Len(t, user2Peers, 1, "user2 should have 1 peer initially (user1 with tag:web)")
|
require.Len(t, user2Peers, 1, "user2 should have 1 peer initially (user1 with tag:web)")
|
||||||
require.Equal(t, user1Node.ID, user2Peers[0].ID(), "user2's peer should be user1")
|
require.Equal(t, user1Node.ID, user2Peers[0], "user2's peer should be user1")
|
||||||
|
|
||||||
// Check user1's peers - should include user2 (bidirectional ACL)
|
// Check user1's peers - should include user2 (bidirectional ACL)
|
||||||
user1Peers := initialPeerMap[user1Node.ID]
|
user1Peers := initialPeerMap[user1Node.ID]
|
||||||
require.Len(t, user1Peers, 1, "user1 should have 1 peer initially (user2)")
|
require.Len(t, user1Peers, 1, "user1 should have 1 peer initially (user2)")
|
||||||
require.Equal(t, user2Node.ID, user1Peers[0].ID(), "user1's peer should be user2")
|
require.Equal(t, user2Node.ID, user1Peers[0], "user1's peer should be user2")
|
||||||
|
|
||||||
// Now change user1's tags: remove tag:web, keep only tag:internal
|
// Now change user1's tags: remove tag:web, keep only tag:internal
|
||||||
user1NodeUpdated := &types.Node{
|
user1NodeUpdated := &types.Node{
|
||||||
@@ -1036,17 +1035,15 @@ func TestAutogroupSelfWithAdminOverride(t *testing.T) {
|
|||||||
|
|
||||||
// Admin should see the tagged server as a peer (via group:admin -> *:* rule)
|
// Admin should see the tagged server as a peer (via group:admin -> *:* rule)
|
||||||
adminPeers := peerMap[adminNode.ID]
|
adminPeers := peerMap[adminNode.ID]
|
||||||
require.True(t, slices.ContainsFunc(adminPeers, func(n types.NodeView) bool {
|
require.True(t, slices.Contains(adminPeers, user1TaggedNode.ID),
|
||||||
return n.ID() == user1TaggedNode.ID
|
"admin should see tagged server as peer via *:* rule (issue #2990)")
|
||||||
}), "admin should see tagged server as peer via *:* rule (issue #2990)")
|
|
||||||
|
|
||||||
// Tagged server should also see admin as a peer (symmetric visibility)
|
// Tagged server should also see admin as a peer (symmetric visibility)
|
||||||
// Even though tagged server cannot ACCESS admin, it should still SEE admin
|
// Even though tagged server cannot ACCESS admin, it should still SEE admin
|
||||||
// because admin CAN access it. This is required for proper network operation.
|
// because admin CAN access it. This is required for proper network operation.
|
||||||
taggedPeers := peerMap[user1TaggedNode.ID]
|
taggedPeers := peerMap[user1TaggedNode.ID]
|
||||||
require.True(t, slices.ContainsFunc(taggedPeers, func(n types.NodeView) bool {
|
require.True(t, slices.Contains(taggedPeers, adminNode.ID),
|
||||||
return n.ID() == adminNode.ID
|
"tagged server should see admin as peer (symmetric visibility)")
|
||||||
}), "tagged server should see admin as peer (symmetric visibility)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAutogroupSelfSymmetricVisibility verifies that peer visibility is symmetric:
|
// TestAutogroupSelfSymmetricVisibility verifies that peer visibility is symmetric:
|
||||||
@@ -1109,16 +1106,14 @@ func TestAutogroupSelfSymmetricVisibility(t *testing.T) {
|
|||||||
|
|
||||||
// Device A (user1) should see device B (tag:web) as peer
|
// Device A (user1) should see device B (tag:web) as peer
|
||||||
aPeers := peerMap[deviceA.ID]
|
aPeers := peerMap[deviceA.ID]
|
||||||
require.True(t, slices.ContainsFunc(aPeers, func(n types.NodeView) bool {
|
require.True(t, slices.Contains(aPeers, deviceB.ID),
|
||||||
return n.ID() == deviceB.ID
|
"device A should see device B as peer (user1 -> tag:web rule)")
|
||||||
}), "device A should see device B as peer (user1 -> tag:web rule)")
|
|
||||||
|
|
||||||
// Device B (tag:web) should ALSO see device A as peer (symmetric visibility)
|
// Device B (tag:web) should ALSO see device A as peer (symmetric visibility)
|
||||||
// Even though B cannot ACCESS A, B should still SEE A as a peer
|
// Even though B cannot ACCESS A, B should still SEE A as a peer
|
||||||
bPeers := peerMap[deviceB.ID]
|
bPeers := peerMap[deviceB.ID]
|
||||||
require.True(t, slices.ContainsFunc(bPeers, func(n types.NodeView) bool {
|
require.True(t, slices.Contains(bPeers, deviceA.ID),
|
||||||
return n.ID() == deviceA.ID
|
"device B should see device A as peer (symmetric visibility)")
|
||||||
}), "device B should see device A as peer (symmetric visibility)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAutogroupSelfDoesNotBreakOtherUsersAccess reproduces the Discord scenario
|
// TestAutogroupSelfDoesNotBreakOtherUsersAccess reproduces the Discord scenario
|
||||||
@@ -1254,11 +1249,7 @@ func TestAutogroupSelfDoesNotBreakOtherUsersAccess(t *testing.T) {
|
|||||||
|
|
||||||
// Helper to check if node A sees node B
|
// Helper to check if node A sees node B
|
||||||
canSee := func(a, b types.NodeID) bool {
|
canSee := func(a, b types.NodeID) bool {
|
||||||
peers := peerMap[a]
|
return slices.Contains(peerMap[a], b)
|
||||||
|
|
||||||
return slices.ContainsFunc(peers, func(n types.NodeView) bool {
|
|
||||||
return n.ID() == b
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Superadmin should see all tagged servers
|
// Superadmin should see all tagged servers
|
||||||
@@ -1358,16 +1349,14 @@ func TestEmptyFilterNodesStillVisible(t *testing.T) {
|
|||||||
|
|
||||||
// Admin should see the tagged server
|
// Admin should see the tagged server
|
||||||
adminPeers := peerMap[adminDevice.ID]
|
adminPeers := peerMap[adminDevice.ID]
|
||||||
require.True(t, slices.ContainsFunc(adminPeers, func(n types.NodeView) bool {
|
require.True(t, slices.Contains(adminPeers, taggedServer.ID),
|
||||||
return n.ID() == taggedServer.ID
|
"admin should see tagged server")
|
||||||
}), "admin should see tagged server")
|
|
||||||
|
|
||||||
// Tagged server should see admin (symmetric visibility)
|
// Tagged server should see admin (symmetric visibility)
|
||||||
// Even though the server has no outbound rules (empty filter)
|
// Even though the server has no outbound rules (empty filter)
|
||||||
serverPeers := peerMap[taggedServer.ID]
|
serverPeers := peerMap[taggedServer.ID]
|
||||||
require.True(t, slices.ContainsFunc(serverPeers, func(n types.NodeView) bool {
|
require.True(t, slices.Contains(serverPeers, adminDevice.ID),
|
||||||
return n.ID() == adminDevice.ID
|
"tagged server should see admin (symmetric visibility)")
|
||||||
}), "tagged server should see admin (symmetric visibility)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAutogroupSelfCombinedWithTags verifies that autogroup:self combined with
|
// TestAutogroupSelfCombinedWithTags verifies that autogroup:self combined with
|
||||||
@@ -1435,11 +1424,7 @@ func TestAutogroupSelfCombinedWithTags(t *testing.T) {
|
|||||||
|
|
||||||
// Helper to check visibility
|
// Helper to check visibility
|
||||||
canSee := func(a, b types.NodeID) bool {
|
canSee := func(a, b types.NodeID) bool {
|
||||||
peers := peerMap[a]
|
return slices.Contains(peerMap[a], b)
|
||||||
|
|
||||||
return slices.ContainsFunc(peers, func(n types.NodeView) bool {
|
|
||||||
return n.ID() == b
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin laptop should see: admin phone (autogroup:self) AND web server (tag:web)
|
// Admin laptop should see: admin phone (autogroup:self) AND web server (tag:web)
|
||||||
@@ -1528,11 +1513,7 @@ func TestIssue2990SameUserTaggedDevice(t *testing.T) {
|
|||||||
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
||||||
|
|
||||||
canSee := func(a, b types.NodeID) bool {
|
canSee := func(a, b types.NodeID) bool {
|
||||||
peers := peerMap[a]
|
return slices.Contains(peerMap[a], b)
|
||||||
|
|
||||||
return slices.ContainsFunc(peers, func(n types.NodeView) bool {
|
|
||||||
return n.ID() == b
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// node1 should see node2 (via group:admin -> *:* and symmetric visibility)
|
// node1 should see node2 (via group:admin -> *:* and symmetric visibility)
|
||||||
@@ -2206,9 +2187,7 @@ func TestBuildPeerMap_AutogroupInternetMakesExitNodeVisible(t *testing.T) {
|
|||||||
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
peerMap := pm.BuildPeerMap(nodes.ViewSlice())
|
||||||
|
|
||||||
require.True(t,
|
require.True(t,
|
||||||
slices.ContainsFunc(peerMap[aliceNode.ID], func(n types.NodeView) bool {
|
slices.Contains(peerMap[aliceNode.ID], exitNode.ID),
|
||||||
return n.ID() == exitNode.ID
|
|
||||||
}),
|
|
||||||
"alice should see the exit node as a peer when an ACL grants autogroup:internet (#3212)")
|
"alice should see the exit node as a peer when an ACL grants autogroup:internet (#3212)")
|
||||||
|
|
||||||
_, matchers := pm.Filter()
|
_, matchers := pm.Filter()
|
||||||
@@ -2528,12 +2507,6 @@ func TestPeerRelayGrantMakesRelayVisible(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
containsID := func(peers []types.NodeView, id types.NodeID) bool {
|
|
||||||
return slices.ContainsFunc(peers, func(nv types.NodeView) bool {
|
|
||||||
return nv.ID() == id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
pm, err := NewPolicyManager(
|
pm, err := NewPolicyManager(
|
||||||
@@ -2544,10 +2517,10 @@ func TestPeerRelayGrantMakesRelayVisible(t *testing.T) {
|
|||||||
peerMap := pm.BuildPeerMap(tt.nodes.ViewSlice())
|
peerMap := pm.BuildPeerMap(tt.nodes.ViewSlice())
|
||||||
|
|
||||||
for _, srcID := range tt.srcIDs {
|
for _, srcID := range tt.srcIDs {
|
||||||
require.True(t, containsID(peerMap[srcID], tt.relayID),
|
require.True(t, slices.Contains(peerMap[srcID], tt.relayID),
|
||||||
"node %d must see relay %d via cap/relay alone",
|
"node %d must see relay %d via cap/relay alone",
|
||||||
srcID, tt.relayID)
|
srcID, tt.relayID)
|
||||||
require.True(t, containsID(peerMap[tt.relayID], srcID),
|
require.True(t, slices.Contains(peerMap[tt.relayID], srcID),
|
||||||
"relay %d must see node %d via cap/relay alone",
|
"relay %d must see node %d via cap/relay alone",
|
||||||
tt.relayID, srcID)
|
tt.relayID, srcID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,13 +148,18 @@ func NewNodeStore(allNodes types.Nodes, peersFunc PeersFunc, batchSize int, batc
|
|||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
// nodesByID is the main source of truth for nodes.
|
// nodesByID is the main source of truth for nodes.
|
||||||
nodesByID map[types.NodeID]types.Node
|
nodesByID map[types.NodeID]types.Node
|
||||||
|
// nodeViewsByID resolves peer adjacency IDs to immutable views from this
|
||||||
|
// snapshot without rebuilding every peer slice on each write.
|
||||||
|
nodeViewsByID map[types.NodeID]types.NodeView
|
||||||
|
|
||||||
// calculated from nodesByID
|
// calculated from nodesByID
|
||||||
nodesByNodeKey map[key.NodePublic]types.NodeView
|
nodesByNodeKey map[key.NodePublic]types.NodeView
|
||||||
nodesByMachineKey map[key.MachinePublic]map[types.UserID]types.NodeView
|
nodesByMachineKey map[key.MachinePublic]map[types.UserID]types.NodeView
|
||||||
peersByNode map[types.NodeID][]types.NodeView
|
// peersByNode stores immutable adjacency as IDs; ListPeers resolves the
|
||||||
nodesByUser map[types.UserID][]types.NodeView
|
// corresponding views through nodeViewsByID.
|
||||||
allNodes []types.NodeView
|
peersByNode map[types.NodeID][]types.NodeID
|
||||||
|
nodesByUser map[types.UserID][]types.NodeView
|
||||||
|
allNodes []types.NodeView
|
||||||
|
|
||||||
// routes maps each prefix to its current primary advertiser. The
|
// routes maps each prefix to its current primary advertiser. The
|
||||||
// previous assignment is carried over when still valid so the
|
// previous assignment is carried over when still valid so the
|
||||||
@@ -167,7 +172,7 @@ type Snapshot struct {
|
|||||||
// with the relationships between nodes and their peers.
|
// with the relationships between nodes and their peers.
|
||||||
// This will typically be used to calculate which nodes can see each other
|
// This will typically be used to calculate which nodes can see each other
|
||||||
// based on the current policy.
|
// based on the current policy.
|
||||||
type PeersFunc func(nodes []types.NodeView) map[types.NodeID][]types.NodeView
|
type PeersFunc func(nodes []types.NodeView) map[types.NodeID][]types.NodeID
|
||||||
|
|
||||||
// work represents a single operation to be performed on the [NodeStore].
|
// work represents a single operation to be performed on the [NodeStore].
|
||||||
type work struct {
|
type work struct {
|
||||||
@@ -617,14 +622,19 @@ func snapshotFromNodes(
|
|||||||
defer timer.ObserveDuration()
|
defer timer.ObserveDuration()
|
||||||
|
|
||||||
allNodes := make([]types.NodeView, 0, len(nodes))
|
allNodes := make([]types.NodeView, 0, len(nodes))
|
||||||
|
nodeViewsByID := make(map[types.NodeID]types.NodeView, len(nodes))
|
||||||
|
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
allNodes = append(allNodes, n.View())
|
nv := n.View()
|
||||||
|
allNodes = append(allNodes, nv)
|
||||||
|
nodeViewsByID[n.ID] = nv
|
||||||
}
|
}
|
||||||
|
|
||||||
routes, isPrimaryRoute := electPrimaryRoutes(nodes, prevRoutes)
|
routes, isPrimaryRoute := electPrimaryRoutes(nodes, prevRoutes)
|
||||||
|
|
||||||
newSnap := Snapshot{
|
newSnap := Snapshot{
|
||||||
nodesByID: nodes,
|
nodesByID: nodes,
|
||||||
|
nodeViewsByID: nodeViewsByID,
|
||||||
allNodes: allNodes,
|
allNodes: allNodes,
|
||||||
nodesByNodeKey: make(map[key.NodePublic]types.NodeView),
|
nodesByNodeKey: make(map[key.NodePublic]types.NodeView),
|
||||||
nodesByMachineKey: make(map[key.MachinePublic]map[types.UserID]types.NodeView),
|
nodesByMachineKey: make(map[key.MachinePublic]map[types.UserID]types.NodeView),
|
||||||
@@ -633,7 +643,7 @@ func snapshotFromNodes(
|
|||||||
// it will use the list of all nodes, combined with the
|
// it will use the list of all nodes, combined with the
|
||||||
// current policy to precalculate which nodes are peers and
|
// current policy to precalculate which nodes are peers and
|
||||||
// can see each other.
|
// can see each other.
|
||||||
peersByNode: func() map[types.NodeID][]types.NodeView {
|
peersByNode: func() map[types.NodeID][]types.NodeID {
|
||||||
peersTimer := prometheus.NewTimer(nodeStorePeersCalculationDuration)
|
peersTimer := prometheus.NewTimer(nodeStorePeersCalculationDuration)
|
||||||
defer peersTimer.ObserveDuration()
|
defer peersTimer.ObserveDuration()
|
||||||
|
|
||||||
@@ -647,7 +657,7 @@ func snapshotFromNodes(
|
|||||||
|
|
||||||
// Build nodesByUser, nodesByNodeKey, and nodesByMachineKey maps
|
// Build nodesByUser, nodesByNodeKey, and nodesByMachineKey maps
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
nodeView := n.View()
|
nodeView := nodeViewsByID[n.ID]
|
||||||
userID := n.TypedUserID()
|
userID := n.TypedUserID()
|
||||||
|
|
||||||
// Tagged nodes are owned by their tags, not a user,
|
// Tagged nodes are owned by their tags, not a user,
|
||||||
@@ -658,7 +668,6 @@ func snapshotFromNodes(
|
|||||||
|
|
||||||
newSnap.nodesByNodeKey[n.NodeKey] = nodeView
|
newSnap.nodesByNodeKey[n.NodeKey] = nodeView
|
||||||
|
|
||||||
// Build machine key index
|
|
||||||
if newSnap.nodesByMachineKey[n.MachineKey] == nil {
|
if newSnap.nodesByMachineKey[n.MachineKey] == nil {
|
||||||
newSnap.nodesByMachineKey[n.MachineKey] = make(map[types.UserID]types.NodeView)
|
newSnap.nodesByMachineKey[n.MachineKey] = make(map[types.UserID]types.NodeView)
|
||||||
}
|
}
|
||||||
@@ -886,7 +895,17 @@ func (s *NodeStore) ListPeers(id types.NodeID) views.Slice[types.NodeView] {
|
|||||||
|
|
||||||
nodeStoreOperations.WithLabelValues("list_peers").Inc()
|
nodeStoreOperations.WithLabelValues("list_peers").Inc()
|
||||||
|
|
||||||
return views.SliceOf(s.data.Load().peersByNode[id])
|
snapshot := s.data.Load()
|
||||||
|
peerIDs := snapshot.peersByNode[id]
|
||||||
|
peers := make([]types.NodeView, 0, len(peerIDs))
|
||||||
|
|
||||||
|
for _, peerID := range peerIDs {
|
||||||
|
if peer, ok := snapshot.nodeViewsByID[peerID]; ok {
|
||||||
|
peers = append(peers, peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return views.SliceOf(peers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrimaryRouteFor returns the current primary advertiser for prefix.
|
// PrimaryRouteFor returns the current primary advertiser for prefix.
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ func TestSnapshotFromNodes(t *testing.T) {
|
|||||||
name: "empty nodes",
|
name: "empty nodes",
|
||||||
setupFunc: func() (map[types.NodeID]types.Node, PeersFunc) {
|
setupFunc: func() (map[types.NodeID]types.Node, PeersFunc) {
|
||||||
nodes := make(map[types.NodeID]types.Node)
|
nodes := make(map[types.NodeID]types.Node)
|
||||||
peersFunc := func(nodes []types.NodeView) map[types.NodeID][]types.NodeView {
|
peersFunc := func(nodes []types.NodeView) map[types.NodeID][]types.NodeID {
|
||||||
return make(map[types.NodeID][]types.NodeView)
|
return make(map[types.NodeID][]types.NodeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes, peersFunc
|
return nodes, peersFunc
|
||||||
@@ -79,9 +79,9 @@ func TestSnapshotFromNodes(t *testing.T) {
|
|||||||
|
|
||||||
// Each node sees the other as peer (but not itself)
|
// Each node sees the other as peer (but not itself)
|
||||||
assert.Len(t, snapshot.peersByNode[1], 1)
|
assert.Len(t, snapshot.peersByNode[1], 1)
|
||||||
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[1][0].ID())
|
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[1][0])
|
||||||
assert.Len(t, snapshot.peersByNode[2], 1)
|
assert.Len(t, snapshot.peersByNode[2], 1)
|
||||||
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[2][0].ID())
|
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[2][0])
|
||||||
assert.Len(t, snapshot.nodesByUser[1], 2)
|
assert.Len(t, snapshot.nodesByUser[1], 2)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -133,17 +133,17 @@ func TestSnapshotFromNodes(t *testing.T) {
|
|||||||
|
|
||||||
// Odd nodes should only see other odd nodes as peers
|
// Odd nodes should only see other odd nodes as peers
|
||||||
require.Len(t, snapshot.peersByNode[1], 1)
|
require.Len(t, snapshot.peersByNode[1], 1)
|
||||||
assert.Equal(t, types.NodeID(3), snapshot.peersByNode[1][0].ID())
|
assert.Equal(t, types.NodeID(3), snapshot.peersByNode[1][0])
|
||||||
|
|
||||||
require.Len(t, snapshot.peersByNode[3], 1)
|
require.Len(t, snapshot.peersByNode[3], 1)
|
||||||
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[3][0].ID())
|
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[3][0])
|
||||||
|
|
||||||
// Even nodes should only see other even nodes as peers
|
// Even nodes should only see other even nodes as peers
|
||||||
require.Len(t, snapshot.peersByNode[2], 1)
|
require.Len(t, snapshot.peersByNode[2], 1)
|
||||||
assert.Equal(t, types.NodeID(4), snapshot.peersByNode[2][0].ID())
|
assert.Equal(t, types.NodeID(4), snapshot.peersByNode[2][0])
|
||||||
|
|
||||||
require.Len(t, snapshot.peersByNode[4], 1)
|
require.Len(t, snapshot.peersByNode[4], 1)
|
||||||
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[4][0].ID())
|
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[4][0])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -190,14 +190,14 @@ func createTestNode(nodeID types.NodeID, userID uint, username, hostname string)
|
|||||||
|
|
||||||
// Peer functions
|
// Peer functions
|
||||||
|
|
||||||
func allowAllPeersFunc(nodes []types.NodeView) map[types.NodeID][]types.NodeView {
|
func allowAllPeersFunc(nodes []types.NodeView) map[types.NodeID][]types.NodeID {
|
||||||
ret := make(map[types.NodeID][]types.NodeView, len(nodes))
|
ret := make(map[types.NodeID][]types.NodeID, len(nodes))
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
var peers []types.NodeView
|
var peers []types.NodeID
|
||||||
|
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n.ID() != node.ID() {
|
if n.ID() != node.ID() {
|
||||||
peers = append(peers, n)
|
peers = append(peers, n.ID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,10 +207,10 @@ func allowAllPeersFunc(nodes []types.NodeView) map[types.NodeID][]types.NodeView
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func oddEvenPeersFunc(nodes []types.NodeView) map[types.NodeID][]types.NodeView {
|
func oddEvenPeersFunc(nodes []types.NodeView) map[types.NodeID][]types.NodeID {
|
||||||
ret := make(map[types.NodeID][]types.NodeView, len(nodes))
|
ret := make(map[types.NodeID][]types.NodeID, len(nodes))
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
var peers []types.NodeView
|
var peers []types.NodeID
|
||||||
|
|
||||||
nodeIsOdd := node.ID()%2 == 1
|
nodeIsOdd := node.ID()%2 == 1
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ func oddEvenPeersFunc(nodes []types.NodeView) map[types.NodeID][]types.NodeView
|
|||||||
|
|
||||||
// Only add peer if both are odd or both are even
|
// Only add peer if both are odd or both are even
|
||||||
if nodeIsOdd == peerIsOdd {
|
if nodeIsOdd == peerIsOdd {
|
||||||
peers = append(peers, n)
|
peers = append(peers, n.ID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,9 +313,9 @@ func TestNodeStoreOperations(t *testing.T) {
|
|||||||
|
|
||||||
// Now both nodes should see each other as peers
|
// Now both nodes should see each other as peers
|
||||||
assert.Len(t, snapshot.peersByNode[1], 1)
|
assert.Len(t, snapshot.peersByNode[1], 1)
|
||||||
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[1][0].ID())
|
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[1][0])
|
||||||
assert.Len(t, snapshot.peersByNode[2], 1)
|
assert.Len(t, snapshot.peersByNode[2], 1)
|
||||||
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[2][0].ID())
|
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[2][0])
|
||||||
assert.Len(t, snapshot.nodesByUser[1], 2)
|
assert.Len(t, snapshot.nodesByUser[1], 2)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -382,9 +382,9 @@ func TestNodeStoreOperations(t *testing.T) {
|
|||||||
|
|
||||||
// Remaining nodes should see each other as peers
|
// Remaining nodes should see each other as peers
|
||||||
assert.Len(t, snapshot.peersByNode[1], 1)
|
assert.Len(t, snapshot.peersByNode[1], 1)
|
||||||
assert.Equal(t, types.NodeID(3), snapshot.peersByNode[1][0].ID())
|
assert.Equal(t, types.NodeID(3), snapshot.peersByNode[1][0])
|
||||||
assert.Len(t, snapshot.peersByNode[3], 1)
|
assert.Len(t, snapshot.peersByNode[3], 1)
|
||||||
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[3][0].ID())
|
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[3][0])
|
||||||
|
|
||||||
// User groupings updated
|
// User groupings updated
|
||||||
assert.Len(t, snapshot.nodesByUser[1], 1) // user1 now has only node 1
|
assert.Len(t, snapshot.nodesByUser[1], 1) // user1 now has only node 1
|
||||||
@@ -475,16 +475,16 @@ func TestNodeStoreOperations(t *testing.T) {
|
|||||||
|
|
||||||
// Verify odd-even peer relationships
|
// Verify odd-even peer relationships
|
||||||
require.Len(t, snapshot.peersByNode[1], 1)
|
require.Len(t, snapshot.peersByNode[1], 1)
|
||||||
assert.Equal(t, types.NodeID(3), snapshot.peersByNode[1][0].ID())
|
assert.Equal(t, types.NodeID(3), snapshot.peersByNode[1][0])
|
||||||
|
|
||||||
require.Len(t, snapshot.peersByNode[2], 1)
|
require.Len(t, snapshot.peersByNode[2], 1)
|
||||||
assert.Equal(t, types.NodeID(4), snapshot.peersByNode[2][0].ID())
|
assert.Equal(t, types.NodeID(4), snapshot.peersByNode[2][0])
|
||||||
|
|
||||||
require.Len(t, snapshot.peersByNode[3], 1)
|
require.Len(t, snapshot.peersByNode[3], 1)
|
||||||
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[3][0].ID())
|
assert.Equal(t, types.NodeID(1), snapshot.peersByNode[3][0])
|
||||||
|
|
||||||
require.Len(t, snapshot.peersByNode[4], 1)
|
require.Len(t, snapshot.peersByNode[4], 1)
|
||||||
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[4][0].ID())
|
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[4][0])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -500,9 +500,9 @@ func TestNodeStoreOperations(t *testing.T) {
|
|||||||
|
|
||||||
// Even nodes should still see each other
|
// Even nodes should still see each other
|
||||||
require.Len(t, snapshot.peersByNode[2], 1)
|
require.Len(t, snapshot.peersByNode[2], 1)
|
||||||
assert.Equal(t, types.NodeID(4), snapshot.peersByNode[2][0].ID())
|
assert.Equal(t, types.NodeID(4), snapshot.peersByNode[2][0])
|
||||||
require.Len(t, snapshot.peersByNode[4], 1)
|
require.Len(t, snapshot.peersByNode[4], 1)
|
||||||
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[4][0].ID())
|
assert.Equal(t, types.NodeID(2), snapshot.peersByNode[4][0])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1245,16 +1245,16 @@ func TestRebuildPeerMapsWithChangedPeersFunc(t *testing.T) {
|
|||||||
|
|
||||||
// This simulates how PolicyManager.BuildPeerMap works - it reads state
|
// This simulates how PolicyManager.BuildPeerMap works - it reads state
|
||||||
// that can change between calls
|
// that can change between calls
|
||||||
dynamicPeersFunc := func(nodes []types.NodeView) map[types.NodeID][]types.NodeView {
|
dynamicPeersFunc := func(nodes []types.NodeView) map[types.NodeID][]types.NodeID {
|
||||||
ret := make(map[types.NodeID][]types.NodeView, len(nodes))
|
ret := make(map[types.NodeID][]types.NodeID, len(nodes))
|
||||||
if allowPeers {
|
if allowPeers {
|
||||||
// Allow all peers
|
// Allow all peers
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
var peers []types.NodeView
|
var peers []types.NodeID
|
||||||
|
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n.ID() != node.ID() {
|
if n.ID() != node.ID() {
|
||||||
peers = append(peers, n)
|
peers = append(peers, n.ID())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1263,7 +1263,7 @@ func TestRebuildPeerMapsWithChangedPeersFunc(t *testing.T) {
|
|||||||
} else {
|
} else {
|
||||||
// Allow no peers
|
// Allow no peers
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
ret[node.ID()] = []types.NodeView{}
|
ret[node.ID()] = []types.NodeID{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,8 +1285,8 @@ func TestRebuildPeerMapsWithChangedPeersFunc(t *testing.T) {
|
|||||||
snapshot := store.data.Load()
|
snapshot := store.data.Load()
|
||||||
require.Len(t, snapshot.peersByNode[1], 1, "node1 should have 1 peer initially")
|
require.Len(t, snapshot.peersByNode[1], 1, "node1 should have 1 peer initially")
|
||||||
require.Len(t, snapshot.peersByNode[2], 1, "node2 should have 1 peer initially")
|
require.Len(t, snapshot.peersByNode[2], 1, "node2 should have 1 peer initially")
|
||||||
require.Equal(t, types.NodeID(2), snapshot.peersByNode[1][0].ID())
|
require.Equal(t, types.NodeID(2), snapshot.peersByNode[1][0])
|
||||||
require.Equal(t, types.NodeID(1), snapshot.peersByNode[2][0].ID())
|
require.Equal(t, types.NodeID(1), snapshot.peersByNode[2][0])
|
||||||
|
|
||||||
// Now "change the policy" by disabling peers
|
// Now "change the policy" by disabling peers
|
||||||
allowPeers = false
|
allowPeers = false
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ func NewState(cfg *types.Config) (*State, error) {
|
|||||||
// This moves the complex peer relationship logic into the policy package where it belongs.
|
// This moves the complex peer relationship logic into the policy package where it belongs.
|
||||||
nodeStore := NewNodeStore(
|
nodeStore := NewNodeStore(
|
||||||
nodes,
|
nodes,
|
||||||
func(nodes []types.NodeView) map[types.NodeID][]types.NodeView {
|
func(nodes []types.NodeView) map[types.NodeID][]types.NodeID {
|
||||||
return polMan.BuildPeerMap(views.SliceOf(nodes))
|
return polMan.BuildPeerMap(views.SliceOf(nodes))
|
||||||
},
|
},
|
||||||
batchSize,
|
batchSize,
|
||||||
|
|||||||
Reference in New Issue
Block a user