Files
headscale/hscontrol/policy/v2/compiled.go
Kristoffer Dalby 4cca63155d 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
2026-05-19 09:55:22 +02:00

877 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package v2
import (
"fmt"
"net/netip"
"slices"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"go4.org/netipx"
"tailscale.com/tailcfg"
"tailscale.com/types/views"
)
// grantCategory classifies a grant by what per-node work it needs.
type grantCategory int
const (
// grantCategoryRegular requires no per-node work. The pre-compiled
// rules are complete and only need [policyutil.ReduceFilterRules].
grantCategoryRegular grantCategory = iota
// grantCategorySelf has autogroup:self destinations that must be
// expanded per-node to same-user untagged device IPs.
grantCategorySelf
// grantCategoryVia has Via tags that route rules to specific
// nodes based on their tags and advertised routes.
grantCategoryVia
)
// compiledGrant is a grant with its sources already resolved to IP
// addresses. The expensive work (alias → IP resolution) is done once
// here. Extracting rules for a specific node reads from pre-resolved
// data without re-resolving.
type compiledGrant struct {
category grantCategory
// srcIPStrings is the final SrcIPs for non-self rules, with
// nonWildcardSrcs appended to match Tailscale SaaS behavior.
srcIPStrings []string
hasWildcard bool
hasDangerAll bool
// rules are the pre-compiled filter rules for non-self, non-via
// destinations. For regular grants this is the complete output.
// For self grants with mixed destinations (self + other), this
// is the non-self portion only.
rules []tailcfg.FilterRule
// self is non-nil when the grant has autogroup:self destinations.
self *selfGrantData
// via is non-nil when the grant has Via tags.
via *viaGrantData
}
// selfGrantData holds data needed for per-node autogroup:self
// compilation. Sources are already resolved.
type selfGrantData struct {
resolvedSrcs []ResolvedAddresses
internetProtocols []ProtocolPort
app tailcfg.PeerCapMap
}
// viaGrantData holds data needed for per-node via-grant compilation.
// Sources are already resolved into srcIPStrings; destinations are
// pre-resolved into prefixes plus a flag for autogroup:internet, which
// the consumers handle separately because its gate is per-node
// (IsExitNode()) rather than per-prefix overlap.
type viaGrantData struct {
viaTags []Tag
resolvedDsts []netip.Prefix
hasAutoGroupInternet bool
internetProtocols []ProtocolPort
srcIPStrings []string
}
// 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
// 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
// route advertisements and therefore contribute nothing here.
func resolveViaDestinations(
pol *Policy,
users types.Users,
nodes views.Slice[types.NodeView],
dsts Aliases,
) ([]netip.Prefix, bool) {
var (
prefixes []netip.Prefix
hasAutoGroupInternet bool
)
for _, d := range dsts {
if ag, ok := d.(*AutoGroup); ok && ag.Is(AutoGroupInternet) {
hasAutoGroupInternet = true
continue
}
ips, err := d.Resolve(pol, users, nodes)
if err != nil || ips == nil {
continue
}
prefixes = append(prefixes, ips.Prefixes()...)
}
return prefixes, hasAutoGroupInternet
}
// 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
// returned to them).
type userNodeIndex map[uint][]types.NodeView
func buildUserNodeIndex(
nodes views.Slice[types.NodeView],
) userNodeIndex {
idx := make(userNodeIndex)
for _, n := range nodes.All() {
if !n.IsTagged() && n.User().Valid() {
uid := n.User().ID()
idx[uid] = append(idx[uid], n)
}
}
return idx
}
// compileNodeAttrs returns the per-node CapMap derived from policy
// 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
// of attrs.
func (pol *Policy) compileNodeAttrs(
users types.Users,
nodes views.Slice[types.NodeView],
) (map[types.NodeID]tailcfg.NodeCapMap, error) {
empty := map[types.NodeID]tailcfg.NodeCapMap{}
if pol == nil {
return empty, nil
}
if len(pol.NodeAttrs) == 0 && !pol.RandomizeClientPort {
return empty, nil
}
result := make(map[types.NodeID]tailcfg.NodeCapMap)
stamp := func(id types.NodeID, attr tailcfg.NodeCapability) {
capMap, ok := result[id]
if !ok {
capMap = tailcfg.NodeCapMap{}
result[id] = capMap
}
// 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 [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 {
id types.NodeID
ips []netip.Addr
}
nodeList := make([]nodeIPs, 0, nodes.Len())
for _, n := range nodes.All() {
nodeList = append(nodeList, nodeIPs{id: n.ID(), ips: n.IPs()})
}
if pol.RandomizeClientPort {
for _, ni := range nodeList {
stamp(ni.id, tailcfg.NodeAttrRandomizeClientPort)
}
}
for _, na := range pol.NodeAttrs {
if len(na.Attrs) == 0 {
continue
}
resolved, err := na.Targets.Resolve(pol, users, nodes)
if err != nil {
return nil, fmt.Errorf("nodeAttrs target %s: %w", na.Targets, err)
}
if resolved == nil {
continue
}
for _, ni := range nodeList {
if !slices.ContainsFunc(ni.ips, resolved.Contains) {
continue
}
for _, attr := range na.Attrs {
stamp(ni.id, attr)
}
}
}
return result, nil
}
// 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 [Policy.compileFilterRules] and the autogroup:self
// expansion.
func (pol *Policy) compileGrants(
users types.Users,
nodes views.Slice[types.NodeView],
) []compiledGrant {
if pol == nil || (pol.ACLs == nil && pol.Grants == nil) {
return nil
}
grants := pol.Grants
for _, acl := range pol.ACLs {
grants = append(grants, aclToGrants(acl)...)
}
compiled := make([]compiledGrant, 0, len(grants))
for _, grant := range grants {
cg, err := pol.compileOneGrant(grant, users, nodes)
if err != nil {
log.Trace().Err(err).Msg("compiling grant")
continue
}
if cg != nil {
compiled = append(compiled, *cg)
}
}
return compiled
}
// 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.
//
//nolint:gocyclo,cyclop
func (pol *Policy) compileOneGrant(
grant Grant,
users types.Users,
nodes views.Slice[types.NodeView],
) (*compiledGrant, error) {
// Via grants: resolve sources, store deferred data.
if len(grant.Via) > 0 {
return pol.compileOneViaGrant(grant, users, nodes)
}
// Split destinations into self vs other.
var autogroupSelfDests, otherDests []Alias
for _, dest := range grant.Destinations {
if ag, ok := dest.(*AutoGroup); ok && ag.Is(AutoGroupSelf) {
autogroupSelfDests = append(autogroupSelfDests, dest)
} else {
otherDests = append(otherDests, dest)
}
}
// Resolve sources per-alias, tracking non-wildcard sources
// separately so we can preserve their IPs alongside the
// wildcard CGNAT ranges (matching Tailscale SaaS behavior).
resolvedSrcs, nonWildcardSrcs, err := resolveSources(
pol, grant.Sources, users, nodes,
)
if err != nil {
return nil, err
}
// Literally empty src=[] or dst=[] produces no rules.
if len(grant.Sources) == 0 || len(grant.Destinations) == 0 {
return nil, nil //nolint:nilnil
}
if len(resolvedSrcs) == 0 && grant.App == nil {
return nil, nil //nolint:nilnil
}
hasWildcard := sourcesHaveWildcard(grant.Sources)
hasDangerAll := sourcesHaveDangerAll(grant.Sources)
srcIPStrings := buildSrcIPStrings(
resolvedSrcs, nonWildcardSrcs,
hasWildcard, hasDangerAll, nodes,
)
cg := &compiledGrant{
srcIPStrings: srcIPStrings,
hasWildcard: hasWildcard,
hasDangerAll: hasDangerAll,
}
// Compile non-self destination rules (done once, shared).
if len(otherDests) > 0 {
cg.rules = pol.compileOtherDests(
users, nodes, grant, otherDests,
resolvedSrcs, srcIPStrings,
)
}
// Classify and store deferred self data.
switch {
case len(autogroupSelfDests) > 0:
cg.category = grantCategorySelf
cg.self = &selfGrantData{
resolvedSrcs: resolvedSrcs,
internetProtocols: grant.InternetProtocols,
app: grant.App,
}
default:
cg.category = grantCategoryRegular
}
return cg, nil
}
// 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].
func (pol *Policy) compileOneViaGrant(
grant Grant,
users types.Users,
nodes views.Slice[types.NodeView],
) (*compiledGrant, error) {
if len(grant.InternetProtocols) == 0 {
return nil, nil //nolint:nilnil
}
resolvedSrcs, _, err := resolveSources(
pol, grant.Sources, users, nodes,
)
if err != nil {
return nil, err
}
if len(resolvedSrcs) == 0 {
return nil, nil //nolint:nilnil
}
// Build merged SrcIPs.
var srcIPs netipx.IPSetBuilder
for _, ips := range resolvedSrcs {
for _, pref := range ips.Prefixes() {
srcIPs.AddPrefix(pref)
}
}
srcResolved, err := newResolved(&srcIPs)
if err != nil {
return nil, err
}
if srcResolved.Empty() {
return nil, nil //nolint:nilnil
}
hasWildcard := sourcesHaveWildcard(grant.Sources)
hasDangerAll := sourcesHaveDangerAll(grant.Sources)
resolvedDsts, hasAutoGroupInternet := resolveViaDestinations(
pol, users, nodes, grant.Destinations,
)
return &compiledGrant{
category: grantCategoryVia,
via: &viaGrantData{
viaTags: grant.Via,
resolvedDsts: resolvedDsts,
hasAutoGroupInternet: hasAutoGroupInternet,
internetProtocols: grant.InternetProtocols,
srcIPStrings: srcIPsWithRoutes(
srcResolved, hasWildcard, hasDangerAll, nodes,
),
},
}, nil
}
// 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 [Alias.Resolve] again.
func resolveSources(
pol *Policy,
sources Aliases,
users types.Users,
nodes views.Slice[types.NodeView],
) ([]ResolvedAddresses, []ResolvedAddresses, error) {
var all, nonWild []ResolvedAddresses
for i, src := range sources {
if ag, ok := src.(*AutoGroup); ok && ag.Is(AutoGroupSelf) {
return nil, nil, errSelfInSources
}
ips, err := src.Resolve(pol, users, nodes)
if err != nil {
log.Trace().Caller().Err(err).
Msg("resolving source ips")
}
if ips != nil {
all = append(all, ips)
if _, isWildcard := sources[i].(Asterix); !isWildcard {
nonWild = append(nonWild, ips)
}
}
}
return all, nonWild, nil
}
// buildSrcIPStrings builds the final SrcIPs string slice from
// resolved sources, preserving non-wildcard IPs alongside wildcard
// CGNAT ranges to match Tailscale SaaS behavior.
func buildSrcIPStrings(
resolvedSrcs, nonWildcardSrcs []ResolvedAddresses,
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() {
return nil
}
srcIPStrs := srcIPsWithRoutes(
srcResolved, hasWildcard, hasDangerAll, nodes,
)
// When sources include a wildcard (*) alongside explicit
// sources (tags, groups, etc.), Tailscale preserves the
// individual IPs from non-wildcard sources alongside the
// merged CGNAT ranges rather than absorbing them.
if hasWildcard && len(nonWildcardSrcs) > 0 {
seen := make(map[string]bool, len(srcIPStrs))
for _, s := range srcIPStrs {
seen[s] = true
}
for _, ips := range nonWildcardSrcs {
for _, s := range ips.Strings() {
if !seen[s] {
seen[s] = true
srcIPStrs = append(srcIPStrs, s)
}
}
}
}
return srcIPStrs
}
// compileOtherDests compiles filter rules for non-self, non-via
// 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],
grant Grant,
otherDests Aliases,
resolvedSrcs []ResolvedAddresses,
srcIPStrings []string,
) []tailcfg.FilterRule {
var rules []tailcfg.FilterRule
// DstPorts rules from InternetProtocols.
for _, ipp := range grant.InternetProtocols {
destPorts := pol.destinationsToNetPortRange(
users, nodes, otherDests, ipp.Ports,
)
if len(destPorts) > 0 && len(srcIPStrings) > 0 {
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcIPStrings,
DstPorts: destPorts,
IPProto: ipp.Protocol.toIANAProtocolNumbers(),
})
}
}
// CapGrant rules from App.
if grant.App != nil {
capSrcIPStrs := srcIPStrings
// When sources resolved to empty but App is set,
// Tailscale still produces the CapGrant rule with
// empty SrcIPs.
if capSrcIPStrs == nil {
capSrcIPStrs = []string{}
}
var (
capGrants []tailcfg.CapGrant
dstIPStrings []string
)
for _, dst := range otherDests {
ips, err := dst.Resolve(pol, users, nodes)
if err != nil {
continue
}
capGrants = append(capGrants, tailcfg.CapGrant{
Dsts: ips.Prefixes(),
CapMap: grant.App,
})
dstIPStrings = append(dstIPStrings, ips.Strings()...)
}
if len(capGrants) > 0 {
srcPrefixes := make([]netip.Prefix, 0, len(resolvedSrcs)*2)
for _, ips := range resolvedSrcs {
srcPrefixes = append(
srcPrefixes, ips.Prefixes()...,
)
}
rules = append(rules, tailcfg.FilterRule{
SrcIPs: capSrcIPStrs,
CapGrant: capGrants,
})
dstsHaveWildcard := sourcesHaveWildcard(otherDests)
if dstsHaveWildcard {
dstIPStrings = append(
dstIPStrings,
approvedSubnetRoutes(nodes)...,
)
}
rules = append(
rules,
companionCapGrantRules(
dstIPStrings, srcPrefixes, grant.App,
)...,
)
}
}
return rules
}
// hasPerNodeGrants reports whether any [compiledGrant] requires
// per-node filter compilation (via grants or autogroup:self).
func hasPerNodeGrants(grants []compiledGrant) bool {
for i := range grants {
if grants[i].category != grantCategoryRegular {
return true
}
}
return false
}
// 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
for i := range grants {
if grants[i].category == grantCategoryVia {
continue
}
rules = append(rules, grants[i].rules...)
}
return mergeFilterRules(rules)
}
// filterRulesForNode produces unreduced filter rules for a specific
// node by combining pre-compiled global rules with per-node self and
// via rules. Regular grants emit their pre-compiled rules as-is.
// Self grants add autogroup:self expansion. Via grants add
// tag-matched, route-intersected rules.
func filterRulesForNode(
grants []compiledGrant,
node types.NodeView,
userIdx userNodeIndex,
) []tailcfg.FilterRule {
var rules []tailcfg.FilterRule
for i := range grants {
cg := &grants[i]
// Pre-compiled rules apply to all grant categories
// (empty for via-only grants).
rules = append(rules, cg.rules...)
switch cg.category {
case grantCategoryRegular:
// Nothing more to do.
case grantCategorySelf:
rules = append(
rules,
compileAutogroupSelf(cg, node, userIdx)...,
)
case grantCategoryVia:
rules = append(
rules,
compileViaForNode(cg, node)...,
)
}
}
return mergeFilterRules(rules)
}
// compileAutogroupSelf produces filter rules for autogroup:self
// destinations for a specific node. Only called for grants with
// self destinations and only produces rules for untagged nodes.
func compileAutogroupSelf(
cg *compiledGrant,
node types.NodeView,
userIdx userNodeIndex,
) []tailcfg.FilterRule {
if node.IsTagged() || cg.self == nil {
return nil
}
if !node.User().Valid() {
return nil
}
sameUserNodes := userIdx[node.User().ID()]
if len(sameUserNodes) == 0 {
return nil
}
var rules []tailcfg.FilterRule
// Filter sources to only same-user untagged devices.
srcResolved := filterSourcesToSameUser(
cg.self.resolvedSrcs, sameUserNodes,
)
if srcResolved == nil || srcResolved.Empty() {
return nil
}
// DstPorts rules from InternetProtocols.
for _, ipp := range cg.self.internetProtocols {
var destPorts []tailcfg.NetPortRange
for _, n := range sameUserNodes {
for _, port := range ipp.Ports {
for _, ip := range n.IPs() {
destPorts = append(
destPorts,
tailcfg.NetPortRange{
IP: ip.String(),
Ports: port,
},
)
}
}
}
if len(destPorts) > 0 {
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcResolved.Strings(),
DstPorts: destPorts,
IPProto: ipp.Protocol.toIANAProtocolNumbers(),
})
}
}
// CapGrant rules from App.
if cg.self.app != nil {
var (
capGrants []tailcfg.CapGrant
dstIPStrings []string
)
for _, n := range sameUserNodes {
var dsts []netip.Prefix
for _, ip := range n.IPs() {
dsts = append(
dsts,
netip.PrefixFrom(ip, ip.BitLen()),
)
dstIPStrings = append(
dstIPStrings, ip.String(),
)
}
capGrants = append(capGrants, tailcfg.CapGrant{
Dsts: dsts,
CapMap: cg.self.app,
})
}
if len(capGrants) > 0 {
rules = append(rules, tailcfg.FilterRule{
SrcIPs: srcResolved.Strings(),
CapGrant: capGrants,
})
rules = append(
rules,
companionCapGrantRules(
dstIPStrings,
srcResolved.Prefixes(),
cg.self.app,
)...,
)
}
}
return rules
}
// filterSourcesToSameUser intersects resolved source addresses with
// same-user untagged device IPs, returning only the addresses that
// belong to those devices.
func filterSourcesToSameUser(
resolvedSrcs []ResolvedAddresses,
sameUserNodes []types.NodeView,
) ResolvedAddresses {
var srcIPs netipx.IPSetBuilder
for _, ips := range resolvedSrcs {
for _, n := range sameUserNodes {
if slices.ContainsFunc(n.IPs(), ips.Contains) {
n.AppendToIPSet(&srcIPs)
}
}
}
srcResolved, err := newResolved(&srcIPs)
if err != nil {
return nil
}
return srcResolved
}
// compileViaForNode produces via-grant filter rules for a specific
// node. Only produces rules when the node matches one of the via
// tags and advertises routes that match the grant destinations.
func compileViaForNode(
cg *compiledGrant,
node types.NodeView,
) []tailcfg.FilterRule {
if cg.via == nil {
return nil
}
// Check if node matches any via tag.
matchesVia := false
for _, viaTag := range cg.via.viaTags {
if node.HasTag(string(viaTag)) {
matchesVia = true
break
}
}
if !matchesVia {
return nil
}
// [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
for _, dstPrefix := range cg.via.resolvedDsts {
// Equality would reject any broader or narrower dst relative
// to the advertised route. Containment in either direction
// matches the operator's authorisation: a broader dst restricts
// traffic to the subset the router serves; a narrower dst rides
// on a router covering more than the operator asked for. The
// rule emits the literal dst either way because that is what
// the policy authorised.
if slices.ContainsFunc(nodeSubnetRoutes, dstPrefix.Overlaps) {
viaDstPrefixes = append(viaDstPrefixes, dstPrefix)
}
}
// autogroup:internet on a via-tagged exit advertiser becomes a rule
// 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,
util.TheInternet().Prefixes()...,
)
}
if len(viaDstPrefixes) == 0 {
return nil
}
// Build rules using pre-resolved srcIPStrings.
var rules []tailcfg.FilterRule
for _, ipp := range cg.via.internetProtocols {
var destPorts []tailcfg.NetPortRange
for _, prefix := range viaDstPrefixes {
for _, port := range ipp.Ports {
destPorts = append(
destPorts,
tailcfg.NetPortRange{
IP: prefix.String(),
Ports: port,
},
)
}
}
if len(destPorts) > 0 {
rules = append(rules, tailcfg.FilterRule{
SrcIPs: cg.via.srcIPStrings,
DstPorts: destPorts,
IPProto: ipp.Protocol.toIANAProtocolNumbers(),
})
}
}
return rules
}