mirror of
https://github.com/juanfont/headscale.git
synced 2025-11-09 05:34:51 -05:00
state/nodestore: in memory representation of nodes
Initial work on a nodestore which stores all of the nodes and their relations in memory with relationship for peers precalculated. It is a copy-on-write structure, replacing the "snapshot" when a change to the structure occurs. It is optimised for reads, and while batches are not fast, they are grouped together to do less of the expensive peer calculation if there are many changes rapidly. Writes will block until commited, while reads are never blocked. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
committed by
Kristoffer Dalby
parent
38be30b6d4
commit
9d236571f4
339
hscontrol/policy/policy_autoapprove_test.go
Normal file
339
hscontrol/policy/policy_autoapprove_test.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
func TestApproveRoutesWithPolicy_NeverRemovesApprovedRoutes(t *testing.T) {
|
||||
user1 := types.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Name: "testuser@",
|
||||
}
|
||||
user2 := types.User{
|
||||
Model: gorm.Model{ID: 2},
|
||||
Name: "otheruser@",
|
||||
}
|
||||
users := []types.User{user1, user2}
|
||||
|
||||
node1 := &types.Node{
|
||||
ID: 1,
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "test-node",
|
||||
UserID: user1.ID,
|
||||
User: user1,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
IPv4: ptr.To(netip.MustParseAddr("100.64.0.1")),
|
||||
ForcedTags: []string{"tag:test"},
|
||||
}
|
||||
|
||||
node2 := &types.Node{
|
||||
ID: 2,
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "other-node",
|
||||
UserID: user2.ID,
|
||||
User: user2,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
IPv4: ptr.To(netip.MustParseAddr("100.64.0.2")),
|
||||
}
|
||||
|
||||
// Create a policy that auto-approves specific routes
|
||||
policyJSON := `{
|
||||
"groups": {
|
||||
"group:test": ["testuser@"]
|
||||
},
|
||||
"tagOwners": {
|
||||
"tag:test": ["testuser@"]
|
||||
},
|
||||
"acls": [
|
||||
{
|
||||
"action": "accept",
|
||||
"src": ["*"],
|
||||
"dst": ["*:*"]
|
||||
}
|
||||
],
|
||||
"autoApprovers": {
|
||||
"routes": {
|
||||
"10.0.0.0/8": ["testuser@", "tag:test"],
|
||||
"10.1.0.0/24": ["testuser@"],
|
||||
"10.2.0.0/24": ["testuser@"],
|
||||
"192.168.0.0/24": ["tag:test"]
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
pm, err := policyv2.NewPolicyManager([]byte(policyJSON), users, views.SliceOf([]types.NodeView{node1.View(), node2.View()}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
node *types.Node
|
||||
currentApproved []netip.Prefix
|
||||
announcedRoutes []netip.Prefix
|
||||
wantApproved []netip.Prefix
|
||||
wantChanged bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "previously_approved_route_no_longer_advertised_should_remain",
|
||||
node: node1,
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"), // Only this one is still advertised
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.0.0/24"), // Should still be here!
|
||||
},
|
||||
wantChanged: false,
|
||||
description: "Previously approved routes should never be removed even when no longer advertised",
|
||||
},
|
||||
{
|
||||
name: "add_new_auto_approved_route_keeps_old_approved",
|
||||
node: node1,
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.5.0.0/24"), // This was manually approved
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.1.0.0/24"), // New route that should be auto-approved
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.1.0.0/24"), // New auto-approved route (subset of 10.0.0.0/8)
|
||||
netip.MustParsePrefix("10.5.0.0/24"), // Old approved route kept
|
||||
},
|
||||
wantChanged: true,
|
||||
description: "New auto-approved routes should be added while keeping old approved routes",
|
||||
},
|
||||
{
|
||||
name: "no_announced_routes_keeps_all_approved",
|
||||
node: node1,
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
netip.MustParsePrefix("172.16.0.0/16"),
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{}, // No routes announced
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("172.16.0.0/16"),
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
},
|
||||
wantChanged: false,
|
||||
description: "All approved routes should remain when no routes are announced",
|
||||
},
|
||||
{
|
||||
name: "no_changes_when_announced_equals_approved",
|
||||
node: node1,
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
wantChanged: false,
|
||||
description: "No changes should occur when announced routes match approved routes",
|
||||
},
|
||||
{
|
||||
name: "auto_approve_multiple_new_routes",
|
||||
node: node1,
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("172.16.0.0/24"), // This was manually approved
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.2.0.0/24"), // Should be auto-approved (subset of 10.0.0.0/8)
|
||||
netip.MustParsePrefix("192.168.0.0/24"), // Should be auto-approved for tag:test
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.2.0.0/24"), // New auto-approved
|
||||
netip.MustParsePrefix("172.16.0.0/24"), // Original kept
|
||||
netip.MustParsePrefix("192.168.0.0/24"), // New auto-approved
|
||||
},
|
||||
wantChanged: true,
|
||||
description: "Multiple new routes should be auto-approved while keeping existing approved routes",
|
||||
},
|
||||
{
|
||||
name: "node_without_permission_no_auto_approval",
|
||||
node: node2, // Different node without the tag
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"), // This requires tag:test
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"), // Only the original approved route
|
||||
},
|
||||
wantChanged: false,
|
||||
description: "Routes should not be auto-approved for nodes without proper permissions",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotApproved, gotChanged := ApproveRoutesWithPolicy(pm, tt.node.View(), tt.currentApproved, tt.announcedRoutes)
|
||||
|
||||
assert.Equal(t, tt.wantChanged, gotChanged, "changed flag mismatch: %s", tt.description)
|
||||
|
||||
// Sort for comparison since ApproveRoutesWithPolicy sorts the results
|
||||
tsaddr.SortPrefixes(tt.wantApproved)
|
||||
assert.Equal(t, tt.wantApproved, gotApproved, "approved routes mismatch: %s", tt.description)
|
||||
|
||||
// Verify that all previously approved routes are still present
|
||||
for _, prevRoute := range tt.currentApproved {
|
||||
assert.Contains(t, gotApproved, prevRoute,
|
||||
"previously approved route %s was removed - this should never happen", prevRoute)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApproveRoutesWithPolicy_NilAndEmptyCases(t *testing.T) {
|
||||
// Create a basic policy for edge case testing
|
||||
aclPolicy := `
|
||||
{
|
||||
"acls": [
|
||||
{"action": "accept", "src": ["*"], "dst": ["*:*"]},
|
||||
],
|
||||
"autoApprovers": {
|
||||
"routes": {
|
||||
"10.1.0.0/24": ["test@"],
|
||||
},
|
||||
},
|
||||
}`
|
||||
|
||||
pmfs := PolicyManagerFuncsForTest([]byte(aclPolicy))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
currentApproved []netip.Prefix
|
||||
announcedRoutes []netip.Prefix
|
||||
wantApproved []netip.Prefix
|
||||
wantChanged bool
|
||||
}{
|
||||
{
|
||||
name: "nil_policy_manager",
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("192.168.0.0/24"),
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
wantChanged: false,
|
||||
},
|
||||
{
|
||||
name: "nil_current_approved",
|
||||
currentApproved: nil,
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.1.0.0/24"),
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.1.0.0/24"),
|
||||
},
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "nil_announced_routes",
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
announcedRoutes: nil,
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
},
|
||||
wantChanged: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate_approved_routes",
|
||||
currentApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("10.0.0.0/24"), // Duplicate
|
||||
},
|
||||
announcedRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.1.0.0/24"),
|
||||
},
|
||||
wantApproved: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/24"),
|
||||
netip.MustParsePrefix("10.1.0.0/24"),
|
||||
},
|
||||
wantChanged: true,
|
||||
},
|
||||
{
|
||||
name: "empty_slices",
|
||||
currentApproved: []netip.Prefix{},
|
||||
announcedRoutes: []netip.Prefix{},
|
||||
wantApproved: []netip.Prefix{},
|
||||
wantChanged: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
for i, pmf := range pmfs {
|
||||
t.Run(fmt.Sprintf("%s-policy-index%d", tt.name, i), func(t *testing.T) {
|
||||
// Create test user
|
||||
user := types.User{
|
||||
Model: gorm.Model{ID: 1},
|
||||
Name: "test",
|
||||
}
|
||||
users := []types.User{user}
|
||||
|
||||
// Create test node
|
||||
node := types.Node{
|
||||
ID: 1,
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "testnode",
|
||||
UserID: user.ID,
|
||||
User: user,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
IPv4: ptr.To(netip.MustParseAddr("100.64.0.1")),
|
||||
ApprovedRoutes: tt.currentApproved,
|
||||
}
|
||||
nodes := types.Nodes{&node}
|
||||
|
||||
// Create policy manager or use nil if specified
|
||||
var pm PolicyManager
|
||||
var err error
|
||||
if tt.name != "nil_policy_manager" {
|
||||
pm, err = pmf(users, nodes.ViewSlice())
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
pm = nil
|
||||
}
|
||||
|
||||
gotApproved, gotChanged := ApproveRoutesWithPolicy(pm, node.View(), tt.currentApproved, tt.announcedRoutes)
|
||||
|
||||
assert.Equal(t, tt.wantChanged, gotChanged, "changed flag mismatch")
|
||||
|
||||
// Handle nil vs empty slice comparison
|
||||
if tt.wantApproved == nil {
|
||||
assert.Nil(t, gotApproved, "expected nil approved routes")
|
||||
} else {
|
||||
tsaddr.SortPrefixes(tt.wantApproved)
|
||||
assert.Equal(t, tt.wantApproved, gotApproved, "approved routes mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user