mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-06 14:50:07 +09:00
policy/v2: send exit nodes every user's autogroup:self rules
Same as other rules: exit routes contain every self destination. Updates #3493
This commit is contained in:
@@ -2,6 +2,7 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
@@ -704,15 +705,54 @@ func compileAutogroupSelf(
|
|||||||
node types.NodeView,
|
node types.NodeView,
|
||||||
userIdx userNodeIndex,
|
userIdx userNodeIndex,
|
||||||
) []tailcfg.FilterRule {
|
) []tailcfg.FilterRule {
|
||||||
if node.IsTagged() || cg.self == nil {
|
if node.IsTagged() || cg.self == nil || !node.User().Valid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !node.User().Valid() {
|
return compileSelfForUser(cg, userIdx[node.User().ID()])
|
||||||
|
}
|
||||||
|
|
||||||
|
// exitNodeSelfRules returns the autogroup:self rules of every user
|
||||||
|
// other than the node's own for an exit node, whose exit routes contain
|
||||||
|
// every self destination. Only the packet filter needs them: they never
|
||||||
|
// make the exit node a peer, and expanding every user for peer matching
|
||||||
|
// would cost O(users) per exit node on each peer map build.
|
||||||
|
func exitNodeSelfRules(
|
||||||
|
grants []compiledGrant,
|
||||||
|
node types.NodeView,
|
||||||
|
userIdx userNodeIndex,
|
||||||
|
) []tailcfg.FilterRule {
|
||||||
|
if !node.IsExitNode() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sameUserNodes := userIdx[node.User().ID()]
|
var rules []tailcfg.FilterRule
|
||||||
|
|
||||||
|
for i := range grants {
|
||||||
|
cg := &grants[i]
|
||||||
|
if cg.self == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, uid := range slices.Sorted(maps.Keys(userIdx)) {
|
||||||
|
// compileAutogroupSelf already covers an untagged node's own user.
|
||||||
|
if !node.IsTagged() && node.User().Valid() && node.User().ID() == uid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, compileSelfForUser(cg, userIdx[uid])...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileSelfForUser produces the autogroup:self rules for one user's
|
||||||
|
// untagged devices.
|
||||||
|
func compileSelfForUser(
|
||||||
|
cg *compiledGrant,
|
||||||
|
sameUserNodes []types.NodeView,
|
||||||
|
) []tailcfg.FilterRule {
|
||||||
if len(sameUserNodes) == 0 {
|
if len(sameUserNodes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,10 @@ func (pol *Policy) compileFilterRulesForNode(
|
|||||||
grants := pol.compileGrants(users, nodes)
|
grants := pol.compileGrants(users, nodes)
|
||||||
userIdx := buildUserNodeIndex(nodes)
|
userIdx := buildUserNodeIndex(nodes)
|
||||||
|
|
||||||
return filterRulesForNode(grants, node, userIdx)
|
return append(
|
||||||
|
filterRulesForNode(grants, node, userIdx),
|
||||||
|
exitNodeSelfRules(grants, node, userIdx)...,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sshAccept = tailcfg.SSHAction{
|
var sshAccept = tailcfg.SSHAction{
|
||||||
|
|||||||
@@ -738,7 +738,10 @@ func (pm *PolicyManager) filterForNodeLocked(
|
|||||||
if !pm.needsPerNodeFilter {
|
if !pm.needsPerNodeFilter {
|
||||||
unreduced = pm.filter
|
unreduced = pm.filter
|
||||||
} else {
|
} else {
|
||||||
unreduced = pm.filterRulesForNodeLocked(node)
|
unreduced = append(
|
||||||
|
pm.filterRulesForNodeLocked(node),
|
||||||
|
exitNodeSelfRules(pm.compiledGrants, node, pm.userNodeIdx)...,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reduced := policyutil.ReduceFilterRules(node, unreduced)
|
reduced := policyutil.ReduceFilterRules(node, unreduced)
|
||||||
|
|||||||
@@ -1445,6 +1445,92 @@ func TestAutogroupSelfCombinedWithTags(t *testing.T) {
|
|||||||
"web server should see admin phone (symmetric)")
|
"web server should see admin phone (symmetric)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAutogroupSelfRulesReachExitNodes checks that an approved exit
|
||||||
|
// node receives every user's autogroup:self rules: its exit routes
|
||||||
|
// contain every destination, and Tailscale SaaS delivers them.
|
||||||
|
func TestAutogroupSelfRulesReachExitNodes(t *testing.T) {
|
||||||
|
users := types.Users{
|
||||||
|
{ID: 1, Name: "alice", Email: "alice@example.com"},
|
||||||
|
{ID: 2, Name: "bob", Email: "bob@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
alice := &types.Node{
|
||||||
|
ID: 1,
|
||||||
|
Hostname: "alice",
|
||||||
|
User: new(users[0]),
|
||||||
|
UserID: new(users[0].ID),
|
||||||
|
IPv4: ap("100.64.0.1"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
bob := &types.Node{
|
||||||
|
ID: 2,
|
||||||
|
Hostname: "bob",
|
||||||
|
User: new(users[1]),
|
||||||
|
UserID: new(users[1].ID),
|
||||||
|
IPv4: ap("100.64.0.2"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
exit := &types.Node{
|
||||||
|
ID: 3,
|
||||||
|
Hostname: "exit",
|
||||||
|
User: new(users[0]),
|
||||||
|
UserID: new(users[0].ID),
|
||||||
|
IPv4: ap("100.64.0.3"),
|
||||||
|
Tags: []string{"tag:exit"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{RoutableIPs: tsaddr.ExitRoutes()},
|
||||||
|
ApprovedRoutes: tsaddr.ExitRoutes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &types.Node{
|
||||||
|
ID: 4,
|
||||||
|
Hostname: "server",
|
||||||
|
User: new(users[0]),
|
||||||
|
UserID: new(users[0].ID),
|
||||||
|
IPv4: ap("100.64.0.4"),
|
||||||
|
Tags: []string{"tag:server"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := types.Nodes{alice, bob, exit, server}
|
||||||
|
|
||||||
|
policy := `{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:exit": ["alice@example.com"],
|
||||||
|
"tag:server": ["alice@example.com"]
|
||||||
|
},
|
||||||
|
"acls": [
|
||||||
|
{"action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"]}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
pm, err := NewPolicyManager([]byte(policy), users, nodes.ViewSlice())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dsts := func(n *types.Node) []string {
|
||||||
|
rules, err := pm.FilterForNode(n.View())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
for _, r := range rules {
|
||||||
|
for _, dp := range r.DstPorts {
|
||||||
|
out = append(out, dp.IP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
require.ElementsMatch(t, []string{"100.64.0.1", "100.64.0.2"}, dsts(exit),
|
||||||
|
"exit node must get every user's self rule")
|
||||||
|
require.Empty(t, dsts(server),
|
||||||
|
"tagged node without exit routes gets no self rules")
|
||||||
|
require.ElementsMatch(t, []string{"100.64.0.1"}, dsts(alice),
|
||||||
|
"user device gets only its own user's self rule")
|
||||||
|
}
|
||||||
|
|
||||||
// TestIssue2990SameUserTaggedDevice reproduces the exact scenario from issue #2990:
|
// TestIssue2990SameUserTaggedDevice reproduces the exact scenario from issue #2990:
|
||||||
// - One user (user1) who is in group:admin
|
// - One user (user1) who is in group:admin
|
||||||
// - node1: user device (not tagged), belongs to user1
|
// - node1: user device (not tagged), belongs to user1
|
||||||
|
|||||||
Reference in New Issue
Block a user