diff --git a/hscontrol/policy/matcher/export_test.go b/hscontrol/policy/matcher/export_test.go index c218d51a..63ec9f09 100644 --- a/hscontrol/policy/matcher/export_test.go +++ b/hscontrol/policy/matcher/export_test.go @@ -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 } diff --git a/hscontrol/policy/matcher/matcher.go b/hscontrol/policy/matcher/matcher.go index 39e7b573..55b36cd7 100644 --- a/hscontrol/policy/matcher/matcher.go +++ b/hscontrol/policy/matcher/matcher.go @@ -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) } diff --git a/hscontrol/policy/v2/compiled.go b/hscontrol/policy/v2/compiled.go index 900df691..642100c7 100644 --- a/hscontrol/policy/v2/compiled.go +++ b/hscontrol/policy/v2/compiled.go @@ -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 } diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index d27967a8..b2b8fddf 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -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)