policy/v2, policy/matcher: consolidate alias and IP-set helpers

This commit is contained in:
Kristoffer Dalby
2026-06-16 09:05:25 +00:00
parent e6ef1dda4d
commit 03e2d24c79
4 changed files with 47 additions and 162 deletions
+3 -28
View File
@@ -1,38 +1,13 @@
package matcher
import (
"github.com/juanfont/headscale/hscontrol/util"
"go4.org/netipx"
)
// 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
// wider. Callers that need strict validation should pre-validate
// their inputs via [util.ParseIPSet].
func MatchFromStrings(sources, destinations []string) Match {
srcs := new(netipx.IPSetBuilder)
dests := new(netipx.IPSetBuilder)
for _, srcIP := range sources {
set, _ := util.ParseIPSet(srcIP, nil)
srcs.AddSet(set)
return Match{
srcs: buildIPSet(sources),
dests: buildIPSet(destinations),
}
for _, dest := range destinations {
set, _ := util.ParseIPSet(dest, nil)
dests.AddSet(set)
}
srcsSet, _ := srcs.IPSet()
destsSet, _ := dests.IPSet()
match := Match{
srcs: srcsSet,
dests: destsSet,
}
return match
}
+18 -8
View File
@@ -52,14 +52,8 @@ func MatchesFromFilterRules(rules []tailcfg.FilterRule) []Match {
// [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)
for _, srcIP := range rule.SrcIPs {
set, _ := util.ParseIPSet(srcIP, nil)
srcs.AddSet(set)
}
for _, dp := range rule.DstPorts {
set, _ := util.ParseIPSet(dp.IP, nil)
dests.AddSet(set)
@@ -71,15 +65,31 @@ func MatchFromFilterRule(rule tailcfg.FilterRule) Match {
}
}
srcsSet, _ := srcs.IPSet()
destsSet, _ := dests.IPSet()
return Match{
srcs: srcsSet,
srcs: buildIPSet(rule.SrcIPs),
dests: destsSet,
}
}
// buildIPSet parses each string via [util.ParseIPSet] and unions the
// results into a single [netipx.IPSet]. Unparseable entries are silently
// dropped (fail-open): the result is narrower than the input described,
// but never wider.
func buildIPSet(addrs []string) *netipx.IPSet {
builder := new(netipx.IPSetBuilder)
for _, addr := range addrs {
set, _ := util.ParseIPSet(addr, nil)
builder.AddSet(set)
}
set, _ := builder.IPSet()
return set
}
func (m *Match) SrcsContainsIPs(ips ...netip.Addr) bool {
return slices.ContainsFunc(ips, m.srcs.Contains)
}
+17 -23
View File
@@ -339,6 +339,20 @@ func (pol *Policy) compileOneGrant(
return cg, nil
}
// mergeResolvedSrcs merges every prefix from the resolved sources into a
// single [resolved] address set.
func mergeResolvedSrcs(resolvedSrcs []ResolvedAddresses) (resolved, error) {
var b netipx.IPSetBuilder
for _, ips := range resolvedSrcs {
for _, pref := range ips.Prefixes() {
b.AddPrefix(pref)
}
}
return newResolved(&b)
}
// 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].
@@ -363,15 +377,7 @@ func (pol *Policy) compileOneViaGrant(
}
// Build merged SrcIPs.
var srcIPs netipx.IPSetBuilder
for _, ips := range resolvedSrcs {
for _, pref := range ips.Prefixes() {
srcIPs.AddPrefix(pref)
}
}
srcResolved, err := newResolved(&srcIPs)
srcResolved, err := mergeResolvedSrcs(resolvedSrcs)
if err != nil {
return nil, err
}
@@ -445,20 +451,8 @@ func buildSrcIPStrings(
hasWildcard, hasDangerAll bool,
nodes views.Slice[types.NodeView],
) []string {
var merged netipx.IPSetBuilder
for _, ips := range resolvedSrcs {
for _, pref := range ips.Prefixes() {
merged.AddPrefix(pref)
}
}
srcResolved, err := newResolved(&merged)
if err != nil {
return nil
}
if srcResolved.Empty() {
srcResolved, err := mergeResolvedSrcs(resolvedSrcs)
if err != nil || srcResolved.Empty() {
return nil
}
+9 -103
View File
@@ -105,9 +105,6 @@ var nodeAttrUnsupportedCaps = map[tailcfg.NodeCapability]string{
// Policy validation errors.
var (
ErrUnknownAliasType = errors.New("unknown alias type")
ErrUnknownAutoApprover = errors.New("unknown auto approver type")
ErrUnknownOwnerType = errors.New("unknown owner type")
ErrInvalidUsername = errors.New("username must contain @")
ErrUserNotFound = errors.New("user not found")
ErrMultipleUsersFound = errors.New("multiple users found")
@@ -145,8 +142,6 @@ var (
ErrHostNotDefined = errors.New("host not defined in policy")
ErrSSHSourceAliasNotSupported = errors.New("alias not supported for SSH source")
ErrSSHDestAliasNotSupported = errors.New("alias not supported for SSH destination")
ErrUnknownSSHDestAlias = errors.New("unknown SSH destination alias type")
ErrUnknownSSHSrcAlias = errors.New("unknown SSH source alias type")
ErrUnknownField = errors.New("unknown field")
ErrProtocolNoSpecificPorts = errors.New("protocol does not support specific ports")
ErrTestEmptyAssertions = errors.New("test entry must have at least one of \"accept\" or \"deny\"")
@@ -263,26 +258,7 @@ func (a AliasWithPorts) MarshalJSON() ([]byte, error) {
return []byte(`""`), nil
}
var alias string
switch v := a.Alias.(type) {
case *Username:
alias = string(*v)
case *Group:
alias = string(*v)
case *Tag:
alias = string(*v)
case *Host:
alias = string(*v)
case *Prefix:
alias = v.String()
case *AutoGroup:
alias = string(*v)
case Asterix:
alias = "*"
default:
return nil, fmt.Errorf("%w: %T", ErrUnknownAliasType, v)
}
alias := a.String()
// If no ports are specified
if len(a.Ports) == 0 {
@@ -1133,24 +1109,7 @@ func (a *Aliases) MarshalJSON() ([]byte, error) {
aliases := make([]string, len(*a))
for i, alias := range *a {
switch v := alias.(type) {
case *Username:
aliases[i] = string(*v)
case *Group:
aliases[i] = string(*v)
case *Tag:
aliases[i] = string(*v)
case *Host:
aliases[i] = string(*v)
case *Prefix:
aliases[i] = v.String()
case *AutoGroup:
aliases[i] = string(*v)
case Asterix:
aliases[i] = "*"
default:
return nil, fmt.Errorf("%w: %T", ErrUnknownAliasType, v)
}
aliases[i] = alias.String()
}
return json.Marshal(aliases)
@@ -1227,16 +1186,7 @@ func (aa AutoApprovers) MarshalJSON() ([]byte, error) {
approvers := make([]string, len(aa))
for i, approver := range aa {
switch v := approver.(type) {
case *Username:
approvers[i] = string(*v)
case *Tag:
approvers[i] = string(*v)
case *Group:
approvers[i] = string(*v)
default:
return nil, fmt.Errorf("%w: %T", ErrUnknownAutoApprover, v)
}
approvers[i] = approver.String()
}
return json.Marshal(approvers)
@@ -1321,16 +1271,7 @@ func (o Owners) MarshalJSON() ([]byte, error) {
owners := make([]string, len(o))
for i, owner := range o {
switch v := owner.(type) {
case *Username:
owners[i] = string(*v)
case *Group:
owners[i] = string(*v)
case *Tag:
owners[i] = string(*v)
default:
return nil, fmt.Errorf("%w: %T", ErrUnknownOwnerType, v)
}
owners[i] = owner.String()
}
return json.Marshal(owners)
@@ -1525,16 +1466,7 @@ func (to TagOwners) MarshalJSON() ([]byte, error) {
ownerStrs := make([]string, len(owners))
for i, owner := range owners {
switch v := owner.(type) {
case *Username:
ownerStrs[i] = string(*v)
case *Group:
ownerStrs[i] = string(*v)
case *Tag:
ownerStrs[i] = string(*v)
default:
return nil, fmt.Errorf("%w: %T", ErrUnknownOwnerType, v)
}
ownerStrs[i] = owner.String()
}
rawTagOwners[tagStr] = ownerStrs
@@ -3021,22 +2953,9 @@ func (a SSHDstAliases) MarshalJSON() ([]byte, error) {
aliases := make([]string, len(a))
for i, alias := range a {
switch v := alias.(type) {
case *Username:
aliases[i] = string(*v)
case *Tag:
aliases[i] = string(*v)
case *AutoGroup:
aliases[i] = string(*v)
case *Host:
aliases[i] = string(*v)
case Asterix:
// Marshal wildcard as "*" so it gets rejected during unmarshal
// with a proper error message explaining alternatives
aliases[i] = "*"
default:
return nil, fmt.Errorf("%w: %T", ErrUnknownSSHDestAlias, v)
}
// A wildcard renders as "*" so it gets rejected during unmarshal
// with a proper error message explaining alternatives.
aliases[i] = alias.String()
}
return json.Marshal(aliases)
@@ -3050,20 +2969,7 @@ func (a *SSHSrcAliases) MarshalJSON() ([]byte, error) {
aliases := make([]string, len(*a))
for i, alias := range *a {
switch v := alias.(type) {
case *Username:
aliases[i] = string(*v)
case *Group:
aliases[i] = string(*v)
case *Tag:
aliases[i] = string(*v)
case *AutoGroup:
aliases[i] = string(*v)
case Asterix:
aliases[i] = "*"
default:
return nil, fmt.Errorf("%w: %T", ErrUnknownSSHSrcAlias, v)
}
aliases[i] = alias.String()
}
return json.Marshal(aliases)