all: apply godoc [Name] link conventions across comments
Every Go-identifier reference in // and /* */ comments now uses
godoc's [Name] linking syntax so pkg.go.dev and `go doc` render
them as clickable cross-references. No behaviour change.
Pattern applied across the tree:
In-package [Foo], [Foo.Bar]
Cross-package [pkg.Foo], [pkg.Foo.Bar]
Stdlib [netip.Prefix], [errors.Is], [context.Context]
Tailscale [tailcfg.MapResponse], [tailcfg.Node.CapMap],
[tailcfg.NodeAttrSuggestExitNode]
Skip rules:
- File:line refs left as plain text
- HuJSON wire keys inside backtick raw strings untouched
- ACL/policy syntax tokens (tag:foo, autogroup:self, ...) not Go
symbols, left as plain text
- JSON/OIDC wire keys, gorm tags, RFC IPv6 placeholders, markdown
link tags, decorative dividers — all left as-is
This commit is contained in:
parent
17236fd284
commit
4cca63155d
124 changed files with 1037 additions and 1011 deletions
|
|
@ -44,13 +44,13 @@ func MatchesFromFilterRules(rules []tailcfg.FilterRule) []Match {
|
|||
return matches
|
||||
}
|
||||
|
||||
// MatchFromFilterRule derives a Match from a tailcfg.FilterRule. The
|
||||
// destination IP set is the union of DstPorts[].IP and CapGrant[].Dsts:
|
||||
// cap-grant-only rules (e.g. tailscale.com/cap/relay) carry their
|
||||
// destinations in CapGrant.Dsts and would otherwise contribute nothing
|
||||
// to peer-visibility derivation in BuildPeerMap / ReduceNodes, hiding
|
||||
// the cap target from the source unless a companion IP-level rule
|
||||
// also exists.
|
||||
// MatchFromFilterRule derives a [Match] from a [tailcfg.FilterRule]. The
|
||||
// destination IP set is the union of [tailcfg.FilterRule.DstPorts][].IP
|
||||
// and [tailcfg.FilterRule.CapGrant][].Dsts: cap-grant-only rules (e.g.
|
||||
// tailscale.com/cap/relay) carry their destinations in CapGrant.Dsts and
|
||||
// would otherwise contribute nothing to peer-visibility derivation in
|
||||
// [policy.BuildPeerMap] / [policy.ReduceNodes], hiding the cap target
|
||||
// from the source unless a companion IP-level rule also exists.
|
||||
func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
|
||||
srcs := new(netipx.IPSetBuilder)
|
||||
dests := new(netipx.IPSetBuilder)
|
||||
|
|
@ -80,11 +80,11 @@ func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
|
|||
}
|
||||
}
|
||||
|
||||
// MatchFromStrings builds a Match from raw source and destination
|
||||
// MatchFromStrings builds a [Match] from raw source and destination
|
||||
// strings. Unparseable entries are silently dropped (fail-open): the
|
||||
// resulting Match is narrower than the input described, but never
|
||||
// resulting [Match] is narrower than the input described, but never
|
||||
// wider. Callers that need strict validation should pre-validate
|
||||
// their inputs via util.ParseIPSet.
|
||||
// their inputs via [util.ParseIPSet].
|
||||
func MatchFromStrings(sources, destinations []string) Match {
|
||||
srcs := new(netipx.IPSetBuilder)
|
||||
dests := new(netipx.IPSetBuilder)
|
||||
|
|
@ -131,7 +131,7 @@ func (m *Match) DestsOverlapsPrefixes(prefixes ...netip.Prefix) bool {
|
|||
// DestsIsTheInternet reports whether the destination covers "the
|
||||
// internet" — the set represented by autogroup:internet, special-cased
|
||||
// for exit nodes. Returns true if either family's /0 is contained
|
||||
// (0.0.0.0/0 or ::/0), or if dests is a superset of TheInternet(). A
|
||||
// (0.0.0.0/0 or ::/0), or if dests is a superset of [util.TheInternet]. A
|
||||
// single-family /0 counts because operators may write it directly and
|
||||
// it still denotes the whole internet for that family.
|
||||
func (m *Match) DestsIsTheInternet() bool {
|
||||
|
|
@ -140,7 +140,7 @@ func (m *Match) DestsIsTheInternet() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Superset-of-TheInternet check handles merged filter rules
|
||||
// Superset-of-[util.TheInternet] check handles merged filter rules
|
||||
// where the internet prefixes are combined with other dests.
|
||||
theInternet := util.TheInternet()
|
||||
for _, prefix := range theInternet.Prefixes() {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ type PolicyManager interface {
|
|||
DebugString() string
|
||||
}
|
||||
|
||||
// NewPolicyManager returns a new policy manager.
|
||||
// NewPolicyManager returns a new [PolicyManager].
|
||||
func NewPolicyManager(pol []byte, users []types.User, nodes views.Slice[types.NodeView]) (PolicyManager, error) {
|
||||
var (
|
||||
polMan PolicyManager
|
||||
|
|
@ -83,8 +83,8 @@ func NewPolicyManager(pol []byte, users []types.User, nodes views.Slice[types.No
|
|||
return polMan, err
|
||||
}
|
||||
|
||||
// PolicyManagersForTest returns all available PostureManagers to be used
|
||||
// in tests to validate them in tests that try to determine that they
|
||||
// PolicyManagersForTest returns all available [PolicyManager] implementations to
|
||||
// be used in tests to validate them in tests that try to determine that they
|
||||
// behave the same.
|
||||
func PolicyManagersForTest(pol []byte, users []types.User, nodes views.Slice[types.NodeView]) ([]PolicyManager, error) {
|
||||
var polMans []PolicyManager
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ func ReduceRoutes(
|
|||
}
|
||||
|
||||
// BuildPeerMap builds a map of all peers that can be accessed by each node.
|
||||
//
|
||||
// Compared to [ReduceNodes], which builds the list per node, we end 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 O(n^2/2)
|
||||
// in the end, but with less work per node.
|
||||
func BuildPeerMap(
|
||||
nodes views.Slice[types.NodeView],
|
||||
matchers []matcher.Match,
|
||||
|
|
@ -58,9 +63,6 @@ func BuildPeerMap(
|
|||
ret := make(map[types.NodeID][]types.NodeView, nodes.Len())
|
||||
|
||||
// Build the map of all peers according to the matchers.
|
||||
// Compared to ReduceNodes, which builds the list per node, we end up with doing
|
||||
// the full work for every node (On^2), while this will reduce 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.
|
||||
for i := range nodes.Len() {
|
||||
for j := i + 1; j < nodes.Len(); j++ {
|
||||
if nodes.At(i).ID() == nodes.At(j).ID() {
|
||||
|
|
@ -78,7 +80,8 @@ func BuildPeerMap(
|
|||
}
|
||||
|
||||
// ApproveRoutesWithPolicy checks if the node can approve the announced routes
|
||||
// and returns the new list of approved routes.
|
||||
// and returns the new list of approved routes. The [PolicyManager] is consulted
|
||||
// via [PolicyManager.NodeCanApproveRoute].
|
||||
// The approved routes will include:
|
||||
// 1. ALL previously approved routes (regardless of whether they're still advertised)
|
||||
// 2. New routes from announcedRoutes that can be auto-approved by policy
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Package policyutil contains pure functions that transform compiled
|
||||
// policy rules for a specific node. The headline function is
|
||||
// ReduceFilterRules, which filters global rules down to those relevant
|
||||
// [ReduceFilterRules], which filters global rules down to those relevant
|
||||
// to one node.
|
||||
//
|
||||
// A node's SubnetRoutes (approved, non-exit) participate in rule
|
||||
// matching so subnet routers receive filter rules for destinations
|
||||
// their subnets cover — the fix for issue #3169.
|
||||
// A node's [types.NodeView.SubnetRoutes] (approved, non-exit) participate
|
||||
// in rule matching so subnet routers receive filter rules for
|
||||
// destinations their subnets cover.
|
||||
package policyutil
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import (
|
|||
//
|
||||
// IMPORTANT: This function is designed for global filters only. Per-node filters
|
||||
// (from autogroup:self policies) are already node-specific and should not be passed
|
||||
// to this function. Use PolicyManager.FilterForNode() instead, which handles both cases.
|
||||
// to this function. Use [policy.PolicyManager.FilterForNode] instead, which handles
|
||||
// both cases.
|
||||
func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcfg.FilterRule {
|
||||
ret := []tailcfg.FilterRule{}
|
||||
subnetRoutes := node.SubnetRoutes()
|
||||
|
|
@ -49,13 +50,14 @@ func ReduceFilterRules(node types.NodeView, rules []tailcfg.FilterRule) []tailcf
|
|||
}
|
||||
|
||||
// If the node has approved subnet routes, preserve
|
||||
// filter rules targeting those routes. SubnetRoutes()
|
||||
// returns only approved, non-exit routes — matching
|
||||
// Tailscale SaaS behavior, which does not generate
|
||||
// filter rules for advertised-but-unapproved routes.
|
||||
// Exit routes (0.0.0.0/0, ::/0) are excluded by
|
||||
// SubnetRoutes() and handled separately via
|
||||
// AllowedIPs/routing.
|
||||
// filter rules targeting those routes.
|
||||
// [types.NodeView.SubnetRoutes] returns only approved,
|
||||
// non-exit routes — matching Tailscale SaaS behavior,
|
||||
// which does not generate filter rules for
|
||||
// advertised-but-unapproved routes. Exit routes
|
||||
// (0.0.0.0/0, ::/0) are excluded by
|
||||
// [types.NodeView.SubnetRoutes] and handled separately
|
||||
// via AllowedIPs/routing.
|
||||
if slices.ContainsFunc(subnetRoutes, expanded.OverlapsPrefix) {
|
||||
dests = append(dests, dest)
|
||||
continue
|
||||
|
|
@ -95,11 +97,11 @@ func ipSetSubsetOf(candidate, container *netipx.IPSet) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// reduceCapGrantRule filters a CapGrant rule to only include CapGrant
|
||||
// entries whose Dsts match the given node's IPs. When a broad prefix
|
||||
// (e.g. 100.64.0.0/10 from dst:*) contains a node's IP, it is
|
||||
// reduceCapGrantRule filters a [tailcfg.CapGrant] rule to only include
|
||||
// [tailcfg.CapGrant] entries whose Dsts match the given node's IPs. When a
|
||||
// broad prefix (e.g. 100.64.0.0/10 from dst:*) contains a node's IP, it is
|
||||
// narrowed to the node's specific /32 or /128 prefix. Returns nil if
|
||||
// no CapGrant entries are relevant to this node.
|
||||
// no [tailcfg.CapGrant] entries are relevant to this node.
|
||||
func reduceCapGrantRule(
|
||||
node types.NodeView,
|
||||
rule tailcfg.FilterRule,
|
||||
|
|
@ -136,9 +138,9 @@ func reduceCapGrantRule(
|
|||
// prefixes to node-specific /32 or /128 so peers receive only
|
||||
// the minimum routing surface. The route-match loop below
|
||||
// preserves the original prefix so the subnet-serving node
|
||||
// receives the full CapGrant scope. SubnetRoutes() excludes
|
||||
// both unapproved and exit routes, matching Tailscale SaaS
|
||||
// behavior.
|
||||
// receives the full CapGrant scope. [types.NodeView.SubnetRoutes]
|
||||
// excludes both unapproved and exit routes, matching Tailscale
|
||||
// SaaS behavior.
|
||||
for _, dst := range cg.Dsts {
|
||||
for _, subnetRoute := range subnetRoutes {
|
||||
if dst.Overlaps(subnetRoute) {
|
||||
|
|
@ -151,7 +153,7 @@ func reduceCapGrantRule(
|
|||
if len(matchingDsts) > 0 {
|
||||
// A Dst can be appended twice when a broad prefix both
|
||||
// contains a node IP and overlaps one of its approved
|
||||
// subnet routes. Sort + Compact dedups; netip.Prefix is
|
||||
// subnet routes. Sort + Compact dedups; [netip.Prefix] is
|
||||
// comparable so Compact works with ==.
|
||||
slices.SortFunc(matchingDsts, netip.Prefix.Compare)
|
||||
matchingDsts = slices.Compact(matchingDsts)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type grantCategory int
|
|||
|
||||
const (
|
||||
// grantCategoryRegular requires no per-node work. The pre-compiled
|
||||
// rules are complete and only need ReduceFilterRules.
|
||||
// rules are complete and only need [policyutil.ReduceFilterRules].
|
||||
grantCategoryRegular grantCategory = iota
|
||||
|
||||
// grantCategorySelf has autogroup:self destinations that must be
|
||||
|
|
@ -80,7 +80,7 @@ type viaGrantData struct {
|
|||
|
||||
// resolveViaDestinations splits a via grant's destinations into the
|
||||
// flat list of IP prefixes they resolve to plus a flag for
|
||||
// autogroup:internet. Every alias kind goes through Alias.Resolve so
|
||||
// autogroup:internet. Every alias kind goes through [Alias.Resolve] so
|
||||
// adding a new alias type to the policy parser does not silently
|
||||
// disappear from the via path. Non-IP alias kinds (tag, user, group,
|
||||
// wildcard) resolve to /32 host IPs that never overlap with subnet
|
||||
|
|
@ -116,7 +116,7 @@ func resolveViaDestinations(
|
|||
|
||||
// userNodeIndex maps user IDs to their untagged nodes. Built once per
|
||||
// policy or node-set change and read from many goroutines under
|
||||
// PolicyManager.mu; readers must hold the lock (or the snapshot
|
||||
// [PolicyManager.mu]; readers must hold the lock (or the snapshot
|
||||
// returned to them).
|
||||
type userNodeIndex map[uint][]types.NodeView
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ func buildUserNodeIndex(
|
|||
}
|
||||
|
||||
// compileNodeAttrs returns the per-node CapMap derived from policy
|
||||
// nodeAttrs plus the tailnet-wide RandomizeClientPort flag.
|
||||
// nodeAttrs plus the tailnet-wide [Policy.RandomizeClientPort] flag.
|
||||
//
|
||||
// Returns an error when a target alias fails to resolve so the caller
|
||||
// surfaces a corrupt policy instead of silently granting a partial set
|
||||
|
|
@ -163,18 +163,18 @@ func (pol *Policy) compileNodeAttrs(
|
|||
result[id] = capMap
|
||||
}
|
||||
|
||||
// nil RawMessage matches the wire format from a Tailscale-hosted
|
||||
// control plane: capabilities without companion data marshal as
|
||||
// `null` rather than `[]`. Storing nil keeps the merge stable
|
||||
// and lets the compat test diff cleanly against captured
|
||||
// netmaps.
|
||||
// nil [tailcfg.RawMessage] matches the wire format from a
|
||||
// Tailscale-hosted control plane: capabilities without companion
|
||||
// data marshal as null rather than []. Storing nil keeps the
|
||||
// merge stable and lets the compat test diff cleanly against
|
||||
// captured netmaps.
|
||||
if _, exists := capMap[attr]; !exists {
|
||||
capMap[attr] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Cache each node's IPs once per call. Without the cache, the
|
||||
// node-attr inner loop would call NodeView.IPs() once per attr
|
||||
// node-attr inner loop would call [types.NodeView.IPs] once per attr
|
||||
// per node — O(grants × nodes) allocations of a 2-element slice
|
||||
// for what is invariant per node within a single policy compile.
|
||||
type nodeIPs struct {
|
||||
|
|
@ -221,10 +221,11 @@ func (pol *Policy) compileNodeAttrs(
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// compileGrants resolves all policy grants into compiledGrant structs.
|
||||
// compileGrants resolves all policy grants into [compiledGrant] structs.
|
||||
// Source resolution and non-self destination resolution happens once
|
||||
// here. This is the single resolution path that replaces the
|
||||
// duplicated work in compileFilterRules and compileGrantWithAutogroupSelf.
|
||||
// duplicated work in [Policy.compileFilterRules] and the autogroup:self
|
||||
// expansion.
|
||||
func (pol *Policy) compileGrants(
|
||||
users types.Users,
|
||||
nodes views.Slice[types.NodeView],
|
||||
|
|
@ -256,7 +257,7 @@ func (pol *Policy) compileGrants(
|
|||
return compiled
|
||||
}
|
||||
|
||||
// compileOneGrant resolves a single grant into a compiledGrant.
|
||||
// compileOneGrant resolves a single grant into a [compiledGrant].
|
||||
// All source resolution happens here. Non-self, non-via destination
|
||||
// resolution also happens here. Per-node data (self dests, via
|
||||
// matching) is stored for deferred compilation.
|
||||
|
|
@ -341,7 +342,7 @@ func (pol *Policy) compileOneGrant(
|
|||
|
||||
// compileOneViaGrant resolves sources for a via grant and stores the
|
||||
// deferred per-node data. The actual via-node matching and route
|
||||
// intersection happens in compileViaForNode.
|
||||
// intersection happens in [compileViaForNode].
|
||||
func (pol *Policy) compileOneViaGrant(
|
||||
grant Grant,
|
||||
users types.Users,
|
||||
|
|
@ -404,8 +405,8 @@ func (pol *Policy) compileOneViaGrant(
|
|||
// resolveSources resolves grant sources per-alias, returning the
|
||||
// resolved addresses and a separate slice of non-wildcard sources.
|
||||
// This is the canonical source-resolution path. Its output lands in
|
||||
// compiledGrant.srcIPStrings (among other places) and callers on the
|
||||
// hot path should prefer reading that over calling Resolve again.
|
||||
// [compiledGrant.srcIPStrings] (among other places) and callers on the
|
||||
// hot path should prefer reading that over calling [Alias.Resolve] again.
|
||||
func resolveSources(
|
||||
pol *Policy,
|
||||
sources Aliases,
|
||||
|
|
@ -490,8 +491,9 @@ func buildSrcIPStrings(
|
|||
}
|
||||
|
||||
// compileOtherDests compiles filter rules for non-self, non-via
|
||||
// destinations. This produces both DstPorts rules (from
|
||||
// InternetProtocols) and CapGrant rules (from App).
|
||||
// destinations. This produces both [tailcfg.FilterRule.DstPorts] rules
|
||||
// (from [Grant.InternetProtocols]) and [tailcfg.CapGrant] rules (from
|
||||
// [Grant.App]).
|
||||
func (pol *Policy) compileOtherDests(
|
||||
users types.Users,
|
||||
nodes views.Slice[types.NodeView],
|
||||
|
|
@ -580,7 +582,7 @@ func (pol *Policy) compileOtherDests(
|
|||
return rules
|
||||
}
|
||||
|
||||
// hasPerNodeGrants reports whether any compiled grant requires
|
||||
// hasPerNodeGrants reports whether any [compiledGrant] requires
|
||||
// per-node filter compilation (via grants or autogroup:self).
|
||||
func hasPerNodeGrants(grants []compiledGrant) bool {
|
||||
for i := range grants {
|
||||
|
|
@ -592,10 +594,10 @@ func hasPerNodeGrants(grants []compiledGrant) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// globalFilterRules extracts global filter rules from compiled
|
||||
// grants. Via grants produce no global rules (they are per-node
|
||||
// only); regular grants contribute their full pre-compiled ruleset;
|
||||
// self grants contribute their non-self portion.
|
||||
// globalFilterRules extracts global filter rules from [compiledGrant]s.
|
||||
// Via grants produce no global rules (they are per-node only); regular
|
||||
// grants contribute their full pre-compiled ruleset; self grants
|
||||
// contribute their non-self portion.
|
||||
func globalFilterRules(grants []compiledGrant) []tailcfg.FilterRule {
|
||||
var rules []tailcfg.FilterRule
|
||||
|
||||
|
|
@ -804,10 +806,11 @@ func compileViaForNode(
|
|||
return nil
|
||||
}
|
||||
|
||||
// SubnetRoutes excludes exit routes, so the overlap gate below sees
|
||||
// only subnet advertisements. autogroup:internet on a via-tagged
|
||||
// exit advertiser is handled separately because its eligibility is
|
||||
// per-node (IsExitNode) rather than per-prefix overlap.
|
||||
// [types.NodeView.SubnetRoutes] excludes exit routes, so the overlap
|
||||
// gate below sees only subnet advertisements. autogroup:internet on
|
||||
// a via-tagged exit advertiser is handled separately because its
|
||||
// eligibility is per-node ([types.NodeView.IsExitNode]) rather than
|
||||
// per-prefix overlap.
|
||||
nodeSubnetRoutes := node.SubnetRoutes()
|
||||
|
||||
var viaDstPrefixes []netip.Prefix
|
||||
|
|
@ -826,11 +829,11 @@ func compileViaForNode(
|
|||
}
|
||||
|
||||
// autogroup:internet on a via-tagged exit advertiser becomes a rule
|
||||
// whose DstPorts enumerate util.TheInternet(). The matchers derived
|
||||
// from this rule let Node.CanAccess surface the exit node to the
|
||||
// grant source via DestsIsTheInternet. ReduceFilterRules strips the
|
||||
// rule from the wire format on non-exit advertisers, preserving
|
||||
// SaaS PacketFilter encoding.
|
||||
// whose DstPorts enumerate [util.TheInternet]. The matchers derived
|
||||
// from this rule let [types.NodeView.CanAccess] surface the exit node
|
||||
// to the grant source via [matcher.Match.DestsIsTheInternet].
|
||||
// [policyutil.ReduceFilterRules] strips the rule from the wire format
|
||||
// on non-exit advertisers, preserving SaaS PacketFilter encoding.
|
||||
if cg.via.hasAutoGroupInternet && node.IsExitNode() {
|
||||
viaDstPrefixes = append(
|
||||
viaDstPrefixes,
|
||||
|
|
|
|||
|
|
@ -735,10 +735,11 @@ func TestTagPropagationToPeerMap(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.NotEmpty(t, matchersForUser2, "MatchersForNode should return non-empty matchers (at least self-access rule)")
|
||||
|
||||
// Test ReduceNodes logic with the updated nodes and matchers
|
||||
// This is what buildTailPeers does - it takes peers from ListPeers (which might include user1)
|
||||
// and filters them using ReduceNodes with the updated matchers
|
||||
// Inline the ReduceNodes logic to avoid import cycle
|
||||
// Test [policy.ReduceNodes] logic with the updated nodes and matchers
|
||||
// This is what [mapper.MapResponseBuilder.buildTailPeers] does - it takes peers from
|
||||
// [state.State.ListPeers] (which might include user1) and filters them using
|
||||
// [policy.ReduceNodes] with the updated matchers
|
||||
// Inline the [policy.ReduceNodes] logic to avoid import cycle
|
||||
user2View := user2Node.View()
|
||||
user1UpdatedView := user1NodeUpdated.View()
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import (
|
|||
// - check: every listed user reaches every dst via a check-action
|
||||
// rule specifically (accept-only matches fail the assertion).
|
||||
|
||||
// SSHPolicyTestResult is the outcome of a single SSHPolicyTest.
|
||||
// SSHPolicyTestResult is the outcome of a single [SSHPolicyTest].
|
||||
type SSHPolicyTestResult struct {
|
||||
Src string `json:"src"`
|
||||
Passed bool `json:"passed"`
|
||||
|
|
@ -122,7 +122,7 @@ func checkFailReason(res SSHPolicyTestResult, user, dst string) string {
|
|||
}
|
||||
|
||||
// RunSSHTests evaluates the live policy's sshTests block and wraps any
|
||||
// failure in errSSHPolicyTestsFailed.
|
||||
// failure in [errSSHPolicyTestsFailed].
|
||||
func (pm *PolicyManager) RunSSHTests() error {
|
||||
if pm == nil || pm.pol == nil || len(pm.pol.SSHTests) == 0 {
|
||||
return nil
|
||||
|
|
@ -162,7 +162,7 @@ func evaluateSSHTests(
|
|||
}
|
||||
|
||||
// runSSHPolicyTests evaluates every sshTests entry. The cache is keyed
|
||||
// by dst NodeID so repeat destinations only compile once per pass.
|
||||
// by dst [types.NodeID] so repeat destinations only compile once per pass.
|
||||
func runSSHPolicyTests(
|
||||
pol *Policy,
|
||||
users []types.User,
|
||||
|
|
@ -389,7 +389,7 @@ func appendUserDst(m map[string][]string, user, dst string) map[string][]string
|
|||
|
||||
// resolveSSHTestSource returns the src's principal addresses and, for
|
||||
// user-shaped sources, the user ID (so autogroup:self can scope to it).
|
||||
// Tag, host, and IP sources return userID 0.
|
||||
// [Tag], [Host], and IP sources return userID 0.
|
||||
func resolveSSHTestSource(
|
||||
src Alias,
|
||||
pol *Policy,
|
||||
|
|
@ -428,9 +428,10 @@ func resolveSSHTestSource(
|
|||
}
|
||||
|
||||
// resolveSSHTestDestNodes maps each dst alias to its destination
|
||||
// NodeViews. autogroup:self needs special handling: it cannot resolve
|
||||
// without per-node context, so it walks the node set keyed on src's
|
||||
// owning user. Other aliases resolve to an IPSet and match via InIPSet.
|
||||
// [types.NodeView]s. autogroup:self needs special handling: it cannot
|
||||
// resolve without per-node context, so it walks the node set keyed on
|
||||
// src's owning user. Other aliases resolve to an [netipx.IPSet] and match
|
||||
// via [types.NodeView.InIPSet].
|
||||
func resolveSSHTestDestNodes(
|
||||
dsts SSHTestDestinations,
|
||||
pol *Policy,
|
||||
|
|
@ -527,8 +528,8 @@ func resolveSSHTestDestNodes(
|
|||
return out, emptyDsts, nil
|
||||
}
|
||||
|
||||
// prefixesToIPSet builds the IPSet that InIPSet expects on the node
|
||||
// side.
|
||||
// prefixesToIPSet builds the [netipx.IPSet] that [types.NodeView.InIPSet]
|
||||
// expects on the node side.
|
||||
func prefixesToIPSet(prefixes []netip.Prefix) (*netipx.IPSet, error) {
|
||||
var b netipx.IPSetBuilder
|
||||
|
||||
|
|
@ -539,9 +540,9 @@ func prefixesToIPSet(prefixes []netip.Prefix) (*netipx.IPSet, error) {
|
|||
return b.IPSet()
|
||||
}
|
||||
|
||||
// compiledSSHPolicy returns the per-node compiled SSH policy, caching
|
||||
// compiledSSHPolicy returns the per-node compiled [tailcfg.SSHPolicy], caching
|
||||
// on miss. baseURL is empty because reachability only checks for the
|
||||
// presence of HoldAndDelegate, not its value.
|
||||
// presence of [tailcfg.SSHAction.HoldAndDelegate], not its value.
|
||||
func compiledSSHPolicy(
|
||||
pol *Policy,
|
||||
users []types.User,
|
||||
|
|
@ -607,8 +608,8 @@ func reachability(
|
|||
return acceptHit, checkHit
|
||||
}
|
||||
|
||||
// principalContainsAddr reports whether any principal's NodeIP matches
|
||||
// srcAddr exactly (the SSH compiler emits one principal per source IP).
|
||||
// principalContainsAddr reports whether any principal's [tailcfg.SSHPrincipal.NodeIP]
|
||||
// matches srcAddr exactly (the SSH compiler emits one principal per source IP).
|
||||
func principalContainsAddr(
|
||||
principals []*tailcfg.SSHPrincipal,
|
||||
srcAddr netip.Addr,
|
||||
|
|
@ -635,7 +636,7 @@ func principalContainsAddr(
|
|||
return false
|
||||
}
|
||||
|
||||
// sshUserMapAllows reports whether SSHUsers permits user. The SSHUsers
|
||||
// sshUserMapAllows reports whether [SSHUsers] permits user. The [SSHUsers]
|
||||
// wire shape (see filter.go compileSSHPolicy):
|
||||
//
|
||||
// - SSHUsers["root"] == "root" allows root; == "" disallows it.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ package v2
|
|||
// Tailscale-hosted control plane emits where headscale has no
|
||||
// equivalent concept yet. The compat test in
|
||||
// tailscale_nodeattrs_compat_test.go builds the self-view CapMap via
|
||||
// [types.NodeView.TailNode] -- the same call the mapper makes -- and
|
||||
// [types.Node.TailNode] -- the same call the mapper makes -- and
|
||||
// strips these from BOTH sides before [cmp.Diff]; every other cap is
|
||||
// compared in full as it lands on the wire.
|
||||
//
|
||||
|
|
@ -30,8 +30,9 @@ import (
|
|||
// (suggest-exit-node, dns-subdomain-resolve — see
|
||||
// ipn/ipnlocal/local.go:7534 and node_backend.go:745) are emitted only
|
||||
// when the peer satisfies the cap's emission condition. This function
|
||||
// encodes those conditions; the mapper calls it from buildTailPeers and
|
||||
// the compat test calls it to compute the expected per-peer wire shape.
|
||||
// encodes those conditions; the mapper calls it from
|
||||
// [mapper.MapResponseBuilder.buildTailPeers] and the compat test calls
|
||||
// it to compute the expected per-peer wire shape.
|
||||
func PeerCapMap(peer types.NodeView, peerSelfCaps tailcfg.NodeCapMap) tailcfg.NodeCapMap {
|
||||
if len(peerSelfCaps) == 0 {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
|
||||
// errPolicyTestsFailed and errSSHPolicyTestsFailed share the
|
||||
// "test(s) failed" prefix but stay distinct so callers can use
|
||||
// errors.Is to tell ACL-test and SSH-test failures apart.
|
||||
// [errors.Is] to tell ACL-test and SSH-test failures apart.
|
||||
var (
|
||||
errPolicyTestsFailed = errors.New("test(s) failed")
|
||||
errSSHPolicyTestsFailed = errors.New("test(s) failed")
|
||||
|
|
@ -55,7 +55,7 @@ type PolicyTest struct {
|
|||
|
||||
// SSHPolicyTest is one entry in the policy's `sshTests` block. The
|
||||
// accept/deny/check arrays carry usernames, not destinations — every
|
||||
// listed user is asserted against every entry in Dst.
|
||||
// listed user is asserted against every entry in [SSHPolicyTest.Dst].
|
||||
type SSHPolicyTest struct {
|
||||
// Src is a single source alias (user, group, tag, host, or IP).
|
||||
Src Alias `json:"src"`
|
||||
|
|
@ -78,7 +78,7 @@ type SSHPolicyTest struct {
|
|||
}
|
||||
|
||||
// SSHTestDestinations is the typed list of destination aliases an
|
||||
// sshTests entry targets. validateSSHTestDestination enforces the
|
||||
// sshTests entry targets. [validateSSHTestDestination] enforces the
|
||||
// SSH-specific shape rules (no :port, no CIDR, no autogroup:internet,
|
||||
// known tag).
|
||||
type SSHTestDestinations []Alias
|
||||
|
|
@ -100,7 +100,7 @@ func (d *SSHTestDestinations) UnmarshalJSON(b []byte) error {
|
|||
}
|
||||
|
||||
// UnmarshalJSON parses each typed field. An empty src lands as a nil
|
||||
// Alias so validation surfaces ErrSSHTestEmptySrc rather than a parser
|
||||
// [Alias] so validation surfaces [ErrSSHTestEmptySrc] rather than a parser
|
||||
// failure.
|
||||
func (t *SSHPolicyTest) UnmarshalJSON(b []byte) error {
|
||||
var raw struct {
|
||||
|
|
@ -134,7 +134,7 @@ func (t *SSHPolicyTest) UnmarshalJSON(b []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// PolicyTestResult is the outcome of a single PolicyTest.
|
||||
// PolicyTestResult is the outcome of a single [PolicyTest].
|
||||
type PolicyTestResult struct {
|
||||
Src string `json:"src"`
|
||||
Proto Protocol `json:"proto,omitempty"`
|
||||
|
|
@ -216,7 +216,7 @@ func (pm *PolicyManager) RunTests() error {
|
|||
}
|
||||
|
||||
// evaluateTests runs the `tests` block against a fresh compilation of pol.
|
||||
// It is the user-write sandbox: the live PolicyManager state is left
|
||||
// It is the user-write sandbox: the live [PolicyManager] state is left
|
||||
// untouched, so a failing test rejects the write without side effects.
|
||||
func evaluateTests(pol *Policy, users []types.User, nodes views.Slice[types.NodeView]) error {
|
||||
if pol == nil || len(pol.Tests) == 0 {
|
||||
|
|
@ -262,7 +262,7 @@ func runPolicyTests(pol *Policy, filter []tailcfg.FilterRule, users []types.User
|
|||
return results
|
||||
}
|
||||
|
||||
// runPolicyTest evaluates one PolicyTest.
|
||||
// runPolicyTest evaluates one [PolicyTest].
|
||||
func runPolicyTest(test PolicyTest, pol *Policy, filter []tailcfg.FilterRule, users []types.User, nodes views.Slice[types.NodeView]) PolicyTestResult {
|
||||
res := PolicyTestResult{
|
||||
Src: test.Src,
|
||||
|
|
@ -322,8 +322,8 @@ func runPolicyTest(test PolicyTest, pol *Policy, filter []tailcfg.FilterRule, us
|
|||
return res
|
||||
}
|
||||
|
||||
// resolveTestSource resolves the Src alias of a PolicyTest into a slice of
|
||||
// netip.Prefix. parseAlias + Alias.Resolve cover every alias type the rest
|
||||
// resolveTestSource resolves the Src alias of a [PolicyTest] into a slice of
|
||||
// [netip.Prefix]. [parseAlias] + [Alias.Resolve] cover every alias type the rest
|
||||
// of the policy engine supports, so tests inherit alias semantics for free.
|
||||
func resolveTestSource(src string, pol *Policy, users []types.User, nodes views.Slice[types.NodeView]) ([]netip.Prefix, error) {
|
||||
alias, err := parseAlias(src)
|
||||
|
|
@ -377,13 +377,13 @@ func evalReachability(srcPrefixes []netip.Prefix, dst string, proto Protocol, po
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// parseDestinationAlias is a thin wrapper over AliasWithPorts.UnmarshalJSON
|
||||
// parseDestinationAlias is a thin wrapper over [AliasWithPorts.UnmarshalJSON]
|
||||
// so callers can hand it a bare `"host:port"` string without re-implementing
|
||||
// the parse logic.
|
||||
func parseDestinationAlias(dst string) (*AliasWithPorts, error) {
|
||||
var awp AliasWithPorts
|
||||
|
||||
// AliasWithPorts.UnmarshalJSON expects a quoted JSON string, so wrap.
|
||||
// [AliasWithPorts.UnmarshalJSON] expects a quoted JSON string, so wrap.
|
||||
err := awp.UnmarshalJSON([]byte(`"` + dst + `"`))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -425,9 +425,9 @@ func srcReachesDst(src netip.Prefix, dstPrefixes []netip.Prefix, ports []tailcfg
|
|||
}
|
||||
|
||||
// ruleMatchesSource reports whether the rule's source list contains src.
|
||||
// SrcIPs may be CIDR, single addresses, IP ranges (`a-b`), or `*`; we use
|
||||
// util.ParseIPSet to cover all of those uniformly. Unparseable entries
|
||||
// are skipped (the rule compiler emits well-formed strings, so this is
|
||||
// [tailcfg.FilterRule.SrcIPs] may be CIDR, single addresses, IP ranges (`a-b`),
|
||||
// or `*`; we use [util.ParseIPSet] to cover all of those uniformly. Unparseable
|
||||
// entries are skipped (the rule compiler emits well-formed strings, so this is
|
||||
// defence-in-depth, not error handling).
|
||||
func ruleMatchesSource(rule tailcfg.FilterRule, src netip.Prefix) bool {
|
||||
for _, raw := range rule.SrcIPs {
|
||||
|
|
@ -445,9 +445,10 @@ func ruleMatchesSource(rule tailcfg.FilterRule, src netip.Prefix) bool {
|
|||
}
|
||||
|
||||
// ruleMatchesProto reports whether the rule permits any of requestedProtos.
|
||||
// An unset rule.IPProto means "any protocol" and matches everything.
|
||||
// requestedProtos is the per-test protocol set: a single proto for an
|
||||
// explicit test.Proto, or the default set when test.Proto is empty.
|
||||
// An unset [tailcfg.FilterRule.IPProto] means "any protocol" and matches
|
||||
// everything. requestedProtos is the per-test protocol set: a single proto
|
||||
// for an explicit [PolicyTest.Proto], or the default set when
|
||||
// [PolicyTest.Proto] is empty.
|
||||
func ruleMatchesProto(rule tailcfg.FilterRule, requestedProtos []int) bool {
|
||||
if len(rule.IPProto) == 0 {
|
||||
return true
|
||||
|
|
@ -479,7 +480,7 @@ func ruleAllowsAnyDest(rule tailcfg.FilterRule, dstPrefixes []netip.Prefix, port
|
|||
return false
|
||||
}
|
||||
|
||||
// destEntryMatchesPrefixes reports whether the rule's NetPortRange.IP
|
||||
// destEntryMatchesPrefixes reports whether the rule's [tailcfg.NetPortRange.IP]
|
||||
// (CIDR, single IP, IP range, or "*") covers any prefix in dstPrefixes.
|
||||
func destEntryMatchesPrefixes(dp tailcfg.NetPortRange, dstPrefixes []netip.Prefix) bool {
|
||||
set, err := util.ParseIPSet(dp.IP, nil)
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ var (
|
|||
//
|
||||
// Brackets are only accepted around IPv6 addresses, not IPv4, hostnames, or other alias types.
|
||||
// Bracket stripping reduces both forms to bare "addr:port" or "addr/prefix:port",
|
||||
// which the normal LastIndex(":") split handles correctly because port strings
|
||||
// never contain colons.
|
||||
// which the normal [strings.LastIndex] of ":" split handles correctly because
|
||||
// port strings never contain colons.
|
||||
func splitDestinationAndPort(input string) (string, string, error) {
|
||||
// Handle RFC 3986 bracketed IPv6 (e.g. "[::1]:80" or "[fd7a::1]/128:80,443").
|
||||
// Strip brackets after validation and fall through to normal parsing.
|
||||
|
|
@ -82,7 +82,7 @@ func splitDestinationAndPort(input string) (string, string, error) {
|
|||
return destination, port, nil
|
||||
}
|
||||
|
||||
// parsePortRange parses a port definition string and returns a slice of PortRange structs.
|
||||
// parsePortRange parses a port definition string and returns a slice of [tailcfg.PortRange] structs.
|
||||
func parsePortRange(portDef string) ([]tailcfg.PortRange, error) {
|
||||
if portDef == "*" {
|
||||
return []tailcfg.PortRange{tailcfg.PortRangeAny}, nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue