mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-17 05:52:17 +09:00
policy: consolidate cache-invalidation and evaluation helpers
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
|
"tailscale.com/util/set"
|
||||||
)
|
)
|
||||||
|
|
||||||
// grantCategory classifies a grant by what per-node work it needs.
|
// grantCategory classifies a grant by what per-node work it needs.
|
||||||
@@ -324,17 +325,15 @@ func (pol *Policy) compileOneGrant(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Classify and store deferred self data.
|
// Classify and store deferred self data. The struct literal already
|
||||||
switch {
|
// initializes category to grantCategoryRegular (the zero value).
|
||||||
case len(autogroupSelfDests) > 0:
|
if len(autogroupSelfDests) > 0 {
|
||||||
cg.category = grantCategorySelf
|
cg.category = grantCategorySelf
|
||||||
cg.self = &selfGrantData{
|
cg.self = &selfGrantData{
|
||||||
resolvedSrcs: resolvedSrcs,
|
resolvedSrcs: resolvedSrcs,
|
||||||
internetProtocols: grant.InternetProtocols,
|
internetProtocols: grant.InternetProtocols,
|
||||||
app: grant.App,
|
app: grant.App,
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
cg.category = grantCategoryRegular
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cg, nil
|
return cg, nil
|
||||||
@@ -472,15 +471,12 @@ func buildSrcIPStrings(
|
|||||||
// individual IPs from non-wildcard sources alongside the
|
// individual IPs from non-wildcard sources alongside the
|
||||||
// merged CGNAT ranges rather than absorbing them.
|
// merged CGNAT ranges rather than absorbing them.
|
||||||
if hasWildcard && len(nonWildcardSrcs) > 0 {
|
if hasWildcard && len(nonWildcardSrcs) > 0 {
|
||||||
seen := make(map[string]bool, len(srcIPStrs))
|
seen := set.SetOf(srcIPStrs)
|
||||||
for _, s := range srcIPStrs {
|
|
||||||
seen[s] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ips := range nonWildcardSrcs {
|
for _, ips := range nonWildcardSrcs {
|
||||||
for _, s := range ips.Strings() {
|
for _, s := range ips.Strings() {
|
||||||
if !seen[s] {
|
if !seen.Contains(s) {
|
||||||
seen[s] = true
|
seen.Add(s)
|
||||||
srcIPStrs = append(srcIPStrs, s)
|
srcIPStrs = append(srcIPStrs, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -627,7 +623,7 @@ func collectRelayTargetIPs(grants []compiledGrant) (*netipx.IPSet, error) {
|
|||||||
// traffic through it must recompute when it goes offline. Returns nil when no
|
// traffic through it must recompute when it goes offline. Returns nil when no
|
||||||
// via grants exist.
|
// via grants exist.
|
||||||
func collectViaTargetTags(grants []compiledGrant) map[Tag]struct{} {
|
func collectViaTargetTags(grants []compiledGrant) map[Tag]struct{} {
|
||||||
var tags map[Tag]struct{}
|
tags := make(map[Tag]struct{})
|
||||||
|
|
||||||
for i := range grants {
|
for i := range grants {
|
||||||
if grants[i].via == nil {
|
if grants[i].via == nil {
|
||||||
@@ -635,14 +631,14 @@ func collectViaTargetTags(grants []compiledGrant) map[Tag]struct{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, t := range grants[i].via.viaTags {
|
for _, t := range grants[i].via.viaTags {
|
||||||
if tags == nil {
|
|
||||||
tags = make(map[Tag]struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
tags[t] = struct{}{}
|
tags[t] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(tags) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -23,13 +22,22 @@ var (
|
|||||||
errSelfInSources = errors.New("autogroup:self cannot be used in sources")
|
errSelfInSources = errors.New("autogroup:self cannot be used in sources")
|
||||||
)
|
)
|
||||||
|
|
||||||
// companionCaps maps certain well-known Tailscale capabilities to
|
// companionCap pairs a well-known Tailscale capability with its
|
||||||
|
// companion capability.
|
||||||
|
type companionCap struct {
|
||||||
|
original tailcfg.PeerCapability
|
||||||
|
companion tailcfg.PeerCapability
|
||||||
|
}
|
||||||
|
|
||||||
|
// companionCaps lists certain well-known Tailscale capabilities and
|
||||||
// their companion capability. When a grant includes one of these
|
// their companion capability. When a grant includes one of these
|
||||||
// capabilities, Tailscale automatically generates an additional
|
// capabilities, Tailscale automatically generates an additional
|
||||||
// [tailcfg.FilterRule] with the companion capability and a nil CapMap value.
|
// [tailcfg.FilterRule] with the companion capability and a nil CapMap value.
|
||||||
var companionCaps = map[tailcfg.PeerCapability]tailcfg.PeerCapability{
|
// The slice is ordered by the original capability name so that
|
||||||
tailcfg.PeerCapabilityTaildrive: tailcfg.PeerCapabilityTaildriveSharer,
|
// generated companion rules are emitted deterministically.
|
||||||
tailcfg.PeerCapabilityRelay: tailcfg.PeerCapabilityRelayTarget,
|
var companionCaps = []companionCap{
|
||||||
|
{tailcfg.PeerCapabilityTaildrive, tailcfg.PeerCapabilityTaildriveSharer},
|
||||||
|
{tailcfg.PeerCapabilityRelay, tailcfg.PeerCapabilityRelayTarget},
|
||||||
}
|
}
|
||||||
|
|
||||||
// companionCapGrantRules returns additional [tailcfg.FilterRule]s for any
|
// companionCapGrantRules returns additional [tailcfg.FilterRule]s for any
|
||||||
@@ -48,38 +56,22 @@ func companionCapGrantRules(
|
|||||||
srcPrefixes []netip.Prefix,
|
srcPrefixes []netip.Prefix,
|
||||||
capMap tailcfg.PeerCapMap,
|
capMap tailcfg.PeerCapMap,
|
||||||
) []tailcfg.FilterRule {
|
) []tailcfg.FilterRule {
|
||||||
// Process in deterministic order by original capability name.
|
companions := make([]tailcfg.FilterRule, 0, len(companionCaps))
|
||||||
type pair struct {
|
|
||||||
original tailcfg.PeerCapability
|
|
||||||
companion tailcfg.PeerCapability
|
|
||||||
}
|
|
||||||
|
|
||||||
var pairs []pair
|
for _, c := range companionCaps {
|
||||||
|
if _, ok := capMap[c.original]; ok {
|
||||||
for cap, companion := range companionCaps {
|
companions = append(companions, tailcfg.FilterRule{
|
||||||
if _, ok := capMap[cap]; ok {
|
SrcIPs: dstIPStrings,
|
||||||
pairs = append(pairs, pair{cap, companion})
|
CapGrant: []tailcfg.CapGrant{
|
||||||
}
|
{
|
||||||
}
|
Dsts: srcPrefixes,
|
||||||
|
CapMap: tailcfg.PeerCapMap{
|
||||||
slices.SortFunc(pairs, func(a, b pair) int {
|
c.companion: nil,
|
||||||
return cmp.Compare(a.original, b.original)
|
},
|
||||||
})
|
|
||||||
|
|
||||||
companions := make([]tailcfg.FilterRule, 0, len(pairs))
|
|
||||||
|
|
||||||
for _, p := range pairs {
|
|
||||||
companions = append(companions, tailcfg.FilterRule{
|
|
||||||
SrcIPs: dstIPStrings,
|
|
||||||
CapGrant: []tailcfg.CapGrant{
|
|
||||||
{
|
|
||||||
Dsts: srcPrefixes,
|
|
||||||
CapMap: tailcfg.PeerCapMap{
|
|
||||||
p.companion: nil,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return companions
|
return companions
|
||||||
@@ -238,7 +230,7 @@ func checkPeriodFromRule(rule SSH) time.Duration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshCheck(baseURL string, _ time.Duration) tailcfg.SSHAction {
|
func sshCheck(baseURL string) tailcfg.SSHAction {
|
||||||
holdURL := baseURL + "/machine/ssh/action/$SRC_NODE_ID/to/$DST_NODE_ID?local_user=$LOCAL_USER"
|
holdURL := baseURL + "/machine/ssh/action/$SRC_NODE_ID/to/$DST_NODE_ID?local_user=$LOCAL_USER"
|
||||||
|
|
||||||
return tailcfg.SSHAction{
|
return tailcfg.SSHAction{
|
||||||
@@ -302,7 +294,7 @@ func (pol *Policy) compileSSHPolicy(
|
|||||||
case SSHActionAccept:
|
case SSHActionAccept:
|
||||||
action = sshAccept
|
action = sshAccept
|
||||||
case SSHActionCheck:
|
case SSHActionCheck:
|
||||||
action = sshCheck(baseURL, checkPeriodFromRule(rule))
|
action = sshCheck(baseURL)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"parsing SSH policy, unknown action %q, index: %d: %w",
|
"parsing SSH policy, unknown action %q, index: %d: %w",
|
||||||
@@ -425,12 +417,7 @@ func (pol *Policy) compileSSHPolicy(
|
|||||||
allPrincipals = append(allPrincipals, taggedPrincipals...)
|
allPrincipals = append(allPrincipals, taggedPrincipals...)
|
||||||
|
|
||||||
if len(allPrincipals) > 0 {
|
if len(allPrincipals) > 0 {
|
||||||
rules = append(rules, &tailcfg.SSHRule{
|
appendRules(allPrincipals, 0, false)
|
||||||
Principals: allPrincipals,
|
|
||||||
SSHUsers: baseUserMap,
|
|
||||||
Action: &action,
|
|
||||||
AcceptEnv: acceptEnv,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if hasLocalpart && slices.ContainsFunc(node.IPs(), srcIPs.Contains) {
|
} else if hasLocalpart && slices.ContainsFunc(node.IPs(), srcIPs.Contains) {
|
||||||
|
|||||||
@@ -822,7 +822,7 @@ func (pm *PolicyManager) SetUsers(users []types.User) (bool, error) {
|
|||||||
|
|
||||||
// If SSH policies exist, force a policy change when users are updated
|
// If SSH policies exist, force a policy change when users are updated
|
||||||
// This ensures nodes get updated SSH policies even if other policy hashes didn't change
|
// This ensures nodes get updated SSH policies even if other policy hashes didn't change
|
||||||
if pm.pol != nil && pm.pol.SSHs != nil && len(pm.pol.SSHs) > 0 {
|
if pm.pol != nil && len(pm.pol.SSHs) > 0 {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,15 +878,23 @@ func (pm *PolicyManager) SetNodes(nodes views.Slice[types.NodeView]) (bool, erro
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nodeIDViewMap indexes a slice of node views by node ID. On duplicate IDs the
|
||||||
|
// last view wins, matching the open-coded loops it replaces.
|
||||||
|
func nodeIDViewMap(s views.Slice[types.NodeView]) map[types.NodeID]types.NodeView {
|
||||||
|
m := make(map[types.NodeID]types.NodeView, s.Len())
|
||||||
|
for _, n := range s.All() {
|
||||||
|
m[n.ID()] = n
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
func (pm *PolicyManager) nodesHavePolicyAffectingChanges(newNodes views.Slice[types.NodeView]) bool {
|
func (pm *PolicyManager) nodesHavePolicyAffectingChanges(newNodes views.Slice[types.NodeView]) bool {
|
||||||
if pm.nodes.Len() != newNodes.Len() {
|
if pm.nodes.Len() != newNodes.Len() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
oldNodes := make(map[types.NodeID]types.NodeView, pm.nodes.Len())
|
oldNodes := nodeIDViewMap(pm.nodes)
|
||||||
for _, node := range pm.nodes.All() {
|
|
||||||
oldNodes[node.ID()] = node
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, newNode := range newNodes.All() {
|
for _, newNode := range newNodes.All() {
|
||||||
oldNode, exists := oldNodes[newNode.ID()]
|
oldNode, exists := oldNodes[newNode.ID()]
|
||||||
@@ -1387,15 +1395,8 @@ func (pm *PolicyManager) DebugString() string {
|
|||||||
// the entire cache.
|
// the entire cache.
|
||||||
func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.Slice[types.NodeView]) {
|
func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.Slice[types.NodeView]) {
|
||||||
// Build maps for efficient lookup
|
// Build maps for efficient lookup
|
||||||
oldNodeMap := make(map[types.NodeID]types.NodeView)
|
oldNodeMap := nodeIDViewMap(oldNodes)
|
||||||
for _, node := range oldNodes.All() {
|
newNodeMap := nodeIDViewMap(newNodes)
|
||||||
oldNodeMap[node.ID()] = node
|
|
||||||
}
|
|
||||||
|
|
||||||
newNodeMap := make(map[types.NodeID]types.NodeView)
|
|
||||||
for _, node := range newNodes.All() {
|
|
||||||
newNodeMap[node.ID()] = node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track which users are affected by changes.
|
// Track which users are affected by changes.
|
||||||
// Tagged nodes don't participate in autogroup:self (identity is tag-based),
|
// Tagged nodes don't participate in autogroup:self (identity is tag-based),
|
||||||
@@ -1476,53 +1477,29 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
|
|||||||
// For autogroup:self, we need to clear all nodes belonging to affected users
|
// For autogroup:self, we need to clear all nodes belonging to affected users
|
||||||
// because autogroup:self rules depend on the entire user's device set.
|
// because autogroup:self rules depend on the entire user's device set.
|
||||||
for nodeID := range pm.filterRulesMap {
|
for nodeID := range pm.filterRulesMap {
|
||||||
// Find the user for this cached node
|
// Find the user for this cached node using the already-built indexes.
|
||||||
|
node, ok := newNodeMap[nodeID]
|
||||||
|
if !ok {
|
||||||
|
node, ok = oldNodeMap[nodeID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node not found in either old or new list, clear it.
|
||||||
|
if !ok {
|
||||||
|
delete(pm.filterRulesMap, nodeID)
|
||||||
|
delete(pm.matchersForNodeMap, nodeID)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tagged nodes don't participate in autogroup:self, so their cache
|
||||||
|
// doesn't need user-based invalidation; leave nodeUserID at zero.
|
||||||
var nodeUserID types.UserID
|
var nodeUserID types.UserID
|
||||||
|
if !node.IsTagged() {
|
||||||
found := false
|
nodeUserID = node.TypedUserID()
|
||||||
|
|
||||||
// Check in new nodes first
|
|
||||||
for _, node := range newNodes.All() {
|
|
||||||
if node.ID() == nodeID {
|
|
||||||
// Tagged nodes don't participate in autogroup:self,
|
|
||||||
// so their cache doesn't need user-based invalidation.
|
|
||||||
if node.IsTagged() {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeUserID = node.TypedUserID()
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not found in new nodes, check old nodes
|
// If the owning user is affected, clear this cache entry.
|
||||||
if !found {
|
if _, affected := affectedUsers[nodeUserID]; affected {
|
||||||
for _, node := range oldNodes.All() {
|
|
||||||
if node.ID() == nodeID {
|
|
||||||
if node.IsTagged() {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeUserID = node.TypedUserID()
|
|
||||||
found = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found the user and they're affected, clear this cache entry
|
|
||||||
if found {
|
|
||||||
if _, affected := affectedUsers[nodeUserID]; affected {
|
|
||||||
delete(pm.filterRulesMap, nodeID)
|
|
||||||
delete(pm.matchersForNodeMap, nodeID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Node not found in either old or new list, clear it
|
|
||||||
delete(pm.filterRulesMap, nodeID)
|
delete(pm.filterRulesMap, nodeID)
|
||||||
delete(pm.matchersForNodeMap, nodeID)
|
delete(pm.matchersForNodeMap, nodeID)
|
||||||
}
|
}
|
||||||
@@ -1553,15 +1530,8 @@ func (pm *PolicyManager) invalidateNodeCache(newNodes views.Slice[types.NodeView
|
|||||||
// invalidateGlobalPolicyCache invalidates only nodes whose properties affecting
|
// invalidateGlobalPolicyCache invalidates only nodes whose properties affecting
|
||||||
// [policyutil.ReduceFilterRules] changed. For global policies, each node's filter is independent.
|
// [policyutil.ReduceFilterRules] changed. For global policies, each node's filter is independent.
|
||||||
func (pm *PolicyManager) invalidateGlobalPolicyCache(newNodes views.Slice[types.NodeView]) {
|
func (pm *PolicyManager) invalidateGlobalPolicyCache(newNodes views.Slice[types.NodeView]) {
|
||||||
oldNodeMap := make(map[types.NodeID]types.NodeView)
|
oldNodeMap := nodeIDViewMap(pm.nodes)
|
||||||
for _, node := range pm.nodes.All() {
|
newNodeMap := nodeIDViewMap(newNodes)
|
||||||
oldNodeMap[node.ID()] = node
|
|
||||||
}
|
|
||||||
|
|
||||||
newNodeMap := make(map[types.NodeID]types.NodeView)
|
|
||||||
for _, node := range newNodes.All() {
|
|
||||||
newNodeMap[node.ID()] = node
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalidate nodes whose properties changed
|
// Invalidate nodes whose properties changed
|
||||||
for nodeID, newNode := range newNodeMap {
|
for nodeID, newNode := range newNodeMap {
|
||||||
|
|||||||
@@ -134,11 +134,7 @@ func (pm *PolicyManager) RunSSHTests() error {
|
|||||||
cache := make(map[types.NodeID]*tailcfg.SSHPolicy)
|
cache := make(map[types.NodeID]*tailcfg.SSHPolicy)
|
||||||
results := runSSHPolicyTests(pm.pol, pm.users, pm.nodes, cache)
|
results := runSSHPolicyTests(pm.pol, pm.users, pm.nodes, cache)
|
||||||
|
|
||||||
if results.AllPassed {
|
return wrapTestResult(errSSHPolicyTestsFailed, results.AllPassed, results.Errors)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w:\n%s", errSSHPolicyTestsFailed, results.Errors())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluateSSHTests runs the block against pol without mutating live state.
|
// evaluateSSHTests runs the block against pol without mutating live state.
|
||||||
@@ -154,11 +150,7 @@ func evaluateSSHTests(
|
|||||||
cache := make(map[types.NodeID]*tailcfg.SSHPolicy)
|
cache := make(map[types.NodeID]*tailcfg.SSHPolicy)
|
||||||
results := runSSHPolicyTests(pol, users, nodes, cache)
|
results := runSSHPolicyTests(pol, users, nodes, cache)
|
||||||
|
|
||||||
if results.AllPassed {
|
return wrapTestResult(errSSHPolicyTestsFailed, results.AllPassed, results.Errors)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w:\n%s", errSSHPolicyTestsFailed, results.Errors())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runSSHPolicyTests evaluates every sshTests entry. The cache is keyed
|
// runSSHPolicyTests evaluates every sshTests entry. The cache is keyed
|
||||||
@@ -251,28 +243,21 @@ func runSSHPolicyTest(
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, user := range test.Accept {
|
for _, g := range []struct {
|
||||||
evaluateAssertion(
|
users []SSHUser
|
||||||
pol, users, nodes, cache,
|
kind sshAssertion
|
||||||
srcAddrs, dstNodes, user.String(),
|
}{
|
||||||
assertAccept, &res,
|
{test.Accept, assertAccept},
|
||||||
)
|
{test.Deny, assertDeny},
|
||||||
}
|
{test.Check, assertCheck},
|
||||||
|
} {
|
||||||
for _, user := range test.Deny {
|
for _, user := range g.users {
|
||||||
evaluateAssertion(
|
evaluateAssertion(
|
||||||
pol, users, nodes, cache,
|
pol, users, nodes, cache,
|
||||||
srcAddrs, dstNodes, user.String(),
|
srcAddrs, dstNodes, user.String(),
|
||||||
assertDeny, &res,
|
g.kind, &res,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, user := range test.Check {
|
|
||||||
evaluateAssertion(
|
|
||||||
pol, users, nodes, cache,
|
|
||||||
srcAddrs, dstNodes, user.String(),
|
|
||||||
assertCheck, &res,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|||||||
+30
-36
@@ -196,6 +196,17 @@ func (r PolicyTestResults) Errors() string {
|
|||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapTestResult returns nil when every test passed, otherwise wraps the
|
||||||
|
// rendered failure breakdown in sentinel. errs is passed uncalled so it is
|
||||||
|
// only evaluated on the failure path.
|
||||||
|
func wrapTestResult(sentinel error, allPassed bool, errs func() string) error {
|
||||||
|
if allPassed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%w:\n%s", sentinel, errs())
|
||||||
|
}
|
||||||
|
|
||||||
// RunTests evaluates the policy's own `tests` block against the live compiled
|
// RunTests evaluates the policy's own `tests` block against the live compiled
|
||||||
// filter and returns a wrapped error when any test fails. Callers that need
|
// filter and returns a wrapped error when any test fails. Callers that need
|
||||||
// the per-test breakdown can call runPolicyTests directly.
|
// the per-test breakdown can call runPolicyTests directly.
|
||||||
@@ -208,11 +219,8 @@ func (pm *PolicyManager) RunTests() error {
|
|||||||
defer pm.mu.Unlock()
|
defer pm.mu.Unlock()
|
||||||
|
|
||||||
results := runPolicyTests(pm.pol, pm.filter, pm.users, pm.nodes)
|
results := runPolicyTests(pm.pol, pm.filter, pm.users, pm.nodes)
|
||||||
if results.AllPassed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w:\n%s", errPolicyTestsFailed, results.Errors())
|
return wrapTestResult(errPolicyTestsFailed, results.AllPassed, results.Errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
// evaluateTests runs the `tests` block against a fresh compilation of pol.
|
// evaluateTests runs the `tests` block against a fresh compilation of pol.
|
||||||
@@ -233,11 +241,8 @@ func evaluateTests(pol *Policy, users []types.User, nodes views.Slice[types.Node
|
|||||||
}
|
}
|
||||||
|
|
||||||
results := runPolicyTests(pol, filter, users, nodes)
|
results := runPolicyTests(pol, filter, users, nodes)
|
||||||
if results.AllPassed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%w:\n%s", errPolicyTestsFailed, results.Errors())
|
return wrapTestResult(errPolicyTestsFailed, results.AllPassed, results.Errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runPolicyTests is the pure evaluation function: given a policy, the
|
// runPolicyTests is the pure evaluation function: given a policy, the
|
||||||
@@ -285,39 +290,28 @@ func runPolicyTest(test PolicyTest, pol *Policy, filter []tailcfg.FilterRule, us
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range test.Accept {
|
check := func(dsts []string, wantAllowed bool, ok, fail *[]string) {
|
||||||
allowed, err := evalReachability(srcPrefixes, dst, test.Proto, pol, filter, users, nodes)
|
for _, dst := range dsts {
|
||||||
if err != nil {
|
allowed, err := evalReachability(srcPrefixes, dst, test.Proto, pol, filter, users, nodes)
|
||||||
res.Passed = false
|
if err != nil {
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("error testing %q: %v", dst, err))
|
res.Passed = false
|
||||||
|
res.Errors = append(res.Errors, fmt.Sprintf("error testing %q: %v", dst, err))
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if allowed {
|
if allowed == wantAllowed {
|
||||||
res.AcceptOK = append(res.AcceptOK, dst)
|
*ok = append(*ok, dst)
|
||||||
} else {
|
} else {
|
||||||
res.Passed = false
|
res.Passed = false
|
||||||
res.AcceptFail = append(res.AcceptFail, dst)
|
|
||||||
|
*fail = append(*fail, dst)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dst := range test.Deny {
|
check(test.Accept, true, &res.AcceptOK, &res.AcceptFail)
|
||||||
allowed, err := evalReachability(srcPrefixes, dst, test.Proto, pol, filter, users, nodes)
|
check(test.Deny, false, &res.DenyOK, &res.DenyFail)
|
||||||
if err != nil {
|
|
||||||
res.Passed = false
|
|
||||||
res.Errors = append(res.Errors, fmt.Sprintf("error testing %q: %v", dst, err))
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !allowed {
|
|
||||||
res.DenyOK = append(res.DenyOK, dst)
|
|
||||||
} else {
|
|
||||||
res.Passed = false
|
|
||||||
res.DenyFail = append(res.DenyFail, dst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/views"
|
"tailscale.com/types/views"
|
||||||
"tailscale.com/util/multierr"
|
"tailscale.com/util/multierr"
|
||||||
|
"tailscale.com/util/set"
|
||||||
"tailscale.com/util/slicesx"
|
"tailscale.com/util/slicesx"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -341,15 +342,15 @@ func (a Asterix) resolve(_ *Policy, _ types.Users, _ views.Slice[types.NodeView]
|
|||||||
// IPSet merges overlapping ranges (e.g. 10.0.0.0/8 absorbs
|
// IPSet merges overlapping ranges (e.g. 10.0.0.0/8 absorbs
|
||||||
// 10.33.0.0/16), but Tailscale preserves individual route entries.
|
// 10.33.0.0/16), but Tailscale preserves individual route entries.
|
||||||
func approvedSubnetRoutes(nodes views.Slice[types.NodeView]) []string {
|
func approvedSubnetRoutes(nodes views.Slice[types.NodeView]) []string {
|
||||||
seen := make(map[string]bool)
|
seen := make(set.Set[string])
|
||||||
|
|
||||||
var routes []string
|
var routes []string
|
||||||
|
|
||||||
for _, node := range nodes.All() {
|
for _, node := range nodes.All() {
|
||||||
for _, route := range node.SubnetRoutes() {
|
for _, route := range node.SubnetRoutes() {
|
||||||
s := route.String()
|
s := route.String()
|
||||||
if !seen[s] {
|
if !seen.Contains(s) {
|
||||||
seen[s] = true
|
seen.Add(s)
|
||||||
routes = append(routes, s)
|
routes = append(routes, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user