Experimental implementation of Policy v2 (#2214)

* utility iterator for ipset

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* split policy -> policy and v1

This commit split out the common policy logic and policy implementation
into separate packages.

policy contains functions that are independent of the policy implementation,
this typically means logic that works on tailcfg types and generic formats.
In addition, it defines the PolicyManager interface which the v1 implements.

v1 is a subpackage which implements the PolicyManager using the "original"
policy implementation.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use polivyv1 definitions in integration tests

These can be marshalled back into JSON, which the
new format might not be able to.

Also, just dont change it all to JSON strings for now.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* formatter: breaks lines

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove compareprefix, use tsaddr version

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* remove getacl test, add back autoapprover

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* use policy manager tag handling

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* rename display helper for user

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* introduce policy v2 package

policy v2 is built from the ground up to be stricter
and follow the same pattern for all types of resolvers.

TODO introduce
aliass
resolver

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* wire up policyv2 in integration testing

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* split policy v2 tests into seperate workflow to work around github limit

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* add policy manager output to /debug

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

* update changelog

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>

---------

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2025-03-10 16:20:29 +01:00 committed by GitHub
parent b6fbd37539
commit 87326f5c4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 5883 additions and 2118 deletions

View file

@ -12,6 +12,7 @@ import (
"net/netip"
"os"
"path"
"regexp"
"sort"
"strconv"
"strings"
@ -19,7 +20,7 @@ import (
"github.com/davecgh/go-spew/spew"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/policy"
policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/dockertestutil"
@ -64,12 +65,13 @@ type HeadscaleInContainer struct {
extraPorts []string
caCerts [][]byte
hostPortBindings map[string][]string
aclPolicy *policy.ACLPolicy
aclPolicy *policyv1.ACLPolicy
env map[string]string
tlsCert []byte
tlsKey []byte
filesInContainer []fileInContainer
postgres bool
policyV2 bool
}
// Option represent optional settings that can be given to a
@ -78,7 +80,7 @@ type Option = func(c *HeadscaleInContainer)
// WithACLPolicy adds a hscontrol.ACLPolicy policy to the
// HeadscaleInContainer instance.
func WithACLPolicy(acl *policy.ACLPolicy) Option {
func WithACLPolicy(acl *policyv1.ACLPolicy) Option {
return func(hsic *HeadscaleInContainer) {
if acl == nil {
return
@ -186,6 +188,14 @@ func WithPostgres() Option {
}
}
// WithPolicyV2 tells the integration test to use the new v2 filter.
func WithPolicyV2() Option {
return func(hsic *HeadscaleInContainer) {
hsic.policyV2 = true
hsic.env["HEADSCALE_EXPERIMENTAL_POLICY_V2"] = "1"
}
}
// WithIPAllocationStrategy sets the tests IP Allocation strategy.
func WithIPAllocationStrategy(strategy types.IPAllocationStrategy) Option {
return func(hsic *HeadscaleInContainer) {
@ -403,6 +413,10 @@ func New(
}
if hsic.aclPolicy != nil {
// Rewrite all user entries in the policy to have an @ at the end.
if hsic.policyV2 {
RewritePolicyToV2(hsic.aclPolicy)
}
data, err := json.Marshal(hsic.aclPolicy)
if err != nil {
return nil, fmt.Errorf("failed to marshal ACL Policy to JSON: %w", err)
@ -869,3 +883,50 @@ func (t *HeadscaleInContainer) SendInterrupt() error {
return nil
}
// TODO(kradalby): Remove this function when v1 is deprecated
func rewriteUsersToV2(strs []string) []string {
var result []string
userPattern := regexp.MustCompile(`^user\d+$`)
for _, username := range strs {
parts := strings.Split(username, ":")
if len(parts) == 0 {
result = append(result, username)
continue
}
firstPart := parts[0]
if userPattern.MatchString(firstPart) {
modifiedFirst := firstPart + "@"
if len(parts) > 1 {
rest := strings.Join(parts[1:], ":")
username = modifiedFirst + ":" + rest
} else {
username = modifiedFirst
}
}
result = append(result, username)
}
return result
}
// rewritePolicyToV2 rewrites the policy to v2 format.
// This mostly means adding the @ prefix to user names.
// replaces are done inplace
func RewritePolicyToV2(pol *policyv1.ACLPolicy) {
for idx := range pol.ACLs {
pol.ACLs[idx].Sources = rewriteUsersToV2(pol.ACLs[idx].Sources)
pol.ACLs[idx].Destinations = rewriteUsersToV2(pol.ACLs[idx].Destinations)
}
for idx := range pol.Groups {
pol.Groups[idx] = rewriteUsersToV2(pol.Groups[idx])
}
for idx := range pol.TagOwners {
pol.TagOwners[idx] = rewriteUsersToV2(pol.TagOwners[idx])
}
for idx := range pol.SSHs {
pol.SSHs[idx].Sources = rewriteUsersToV2(pol.SSHs[idx].Sources)
pol.SSHs[idx].Destinations = rewriteUsersToV2(pol.SSHs[idx].Destinations)
}
}