From 065c41d1d8306d050ae47937207fca5eb010d50b Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 14 May 2025 15:14:36 +0200 Subject: [PATCH] integration: start moving to v2 policy Signed-off-by: Kristoffer Dalby --- hscontrol/policy/v2/types.go | 392 +++++++++++++++++++++++--- hscontrol/policy/v2/types_test.go | 111 +++++++- integration/acl_test.go | 440 +++++++++++++++++------------- integration/cli_test.go | 98 ++++--- integration/control.go | 4 +- integration/hsic/hsic.go | 17 +- integration/route_test.go | 236 +++++++++------- integration/scenario.go | 5 - integration/ssh_test.go | 132 +++++---- integration/utils.go | 72 ++++- 10 files changed, 1066 insertions(+), 441 deletions(-) diff --git a/hscontrol/policy/v2/types.go b/hscontrol/policy/v2/types.go index a49f55de..6611d3b8 100644 --- a/hscontrol/policy/v2/types.go +++ b/hscontrol/policy/v2/types.go @@ -32,6 +32,60 @@ func (a Asterix) String() string { return "*" } +// MarshalJSON marshals the Asterix to JSON. +func (a Asterix) MarshalJSON() ([]byte, error) { + return []byte(`"*"`), nil +} + +// MarshalJSON marshals the AliasWithPorts to JSON. +func (a AliasWithPorts) MarshalJSON() ([]byte, error) { + if a.Alias == nil { + 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("unknown alias type: %T", v) + } + + // If no ports are specified + if len(a.Ports) == 0 { + return json.Marshal(alias) + } + + // Check if it's the wildcard port range + if len(a.Ports) == 1 && a.Ports[0].First == 0 && a.Ports[0].Last == 65535 { + return json.Marshal(fmt.Sprintf("%s:*", alias)) + } + + // Otherwise, format as "alias:ports" + var ports []string + for _, port := range a.Ports { + if port.First == port.Last { + ports = append(ports, fmt.Sprintf("%d", port.First)) + } else { + ports = append(ports, fmt.Sprintf("%d-%d", port.First, port.Last)) + } + } + + return json.Marshal(fmt.Sprintf("%s:%s", alias, strings.Join(ports, ","))) +} + func (a Asterix) UnmarshalJSON(b []byte) error { return nil } @@ -62,6 +116,16 @@ func (u *Username) String() string { return string(*u) } +// MarshalJSON marshals the Username to JSON. +func (u Username) MarshalJSON() ([]byte, error) { + return json.Marshal(string(u)) +} + +// MarshalJSON marshals the Prefix to JSON. +func (p Prefix) MarshalJSON() ([]byte, error) { + return json.Marshal(p.String()) +} + func (u *Username) UnmarshalJSON(b []byte) error { *u = Username(strings.Trim(string(b), `"`)) if err := u.Validate(); err != nil { @@ -162,10 +226,25 @@ func (g Group) CanBeAutoApprover() bool { return true } +// String returns the string representation of the Group. func (g Group) String() string { return string(g) } +func (h Host) String() string { + return string(h) +} + +// MarshalJSON marshals the Host to JSON. +func (h Host) MarshalJSON() ([]byte, error) { + return json.Marshal(string(h)) +} + +// MarshalJSON marshals the Group to JSON. +func (g Group) MarshalJSON() ([]byte, error) { + return json.Marshal(string(g)) +} + func (g Group) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) { var ips netipx.IPSetBuilder var errs []error @@ -243,6 +322,11 @@ func (t Tag) String() string { return string(t) } +// MarshalJSON marshals the Tag to JSON. +func (t Tag) MarshalJSON() ([]byte, error) { + return json.Marshal(string(t)) +} + // Host is a string that represents a hostname. type Host string @@ -409,6 +493,11 @@ func (ag *AutoGroup) UnmarshalJSON(b []byte) error { return nil } +// MarshalJSON marshals the AutoGroup to JSON. +func (ag AutoGroup) MarshalJSON() ([]byte, error) { + return json.Marshal(string(ag)) +} + func (ag AutoGroup) Resolve(_ *Policy, _ types.Users, _ types.Nodes) (*netipx.IPSet, error) { switch ag { case AutoGroupInternet: @@ -573,6 +662,37 @@ func (a *Aliases) UnmarshalJSON(b []byte) error { return nil } +// MarshalJSON marshals the Aliases to JSON. +func (a Aliases) MarshalJSON() ([]byte, error) { + if a == nil { + return []byte("[]"), nil + } + + 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("unknown alias type: %T", v) + } + } + + return json.Marshal(aliases) +} + func (a Aliases) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) { var ips netipx.IPSetBuilder var errs []error @@ -631,6 +751,29 @@ func (aa *AutoApprovers) UnmarshalJSON(b []byte) error { return nil } +// MarshalJSON marshals the AutoApprovers to JSON. +func (aa AutoApprovers) MarshalJSON() ([]byte, error) { + if aa == nil { + return []byte("[]"), nil + } + + 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("unknown auto approver type: %T", v) + } + } + + return json.Marshal(approvers) +} + func parseAutoApprover(s string) (AutoApprover, error) { switch { case isUser(s): @@ -700,6 +843,27 @@ func (o *Owners) UnmarshalJSON(b []byte) error { return nil } +// MarshalJSON marshals the Owners to JSON. +func (o Owners) MarshalJSON() ([]byte, error) { + if o == nil { + return []byte("[]"), nil + } + + 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) + default: + return nil, fmt.Errorf("unknown owner type: %T", v) + } + } + + return json.Marshal(owners) +} + func parseOwner(s string) (Owner, error) { switch { case isUser(s): @@ -786,22 +950,64 @@ func (h *Hosts) UnmarshalJSON(b []byte) error { return err } - var pref Prefix - err := pref.parseString(value) - if err != nil { - return fmt.Errorf("Hostname %q contains an invalid IP address: %q", key, value) + var prefix Prefix + if err := prefix.parseString(value); err != nil { + return fmt.Errorf(`Hostname "%s" contains an invalid IP address: "%s"`, key, value) } - (*h)[host] = pref + (*h)[host] = prefix } + return nil } +// MarshalJSON marshals the Hosts to JSON. +func (h Hosts) MarshalJSON() ([]byte, error) { + if h == nil { + return []byte("{}"), nil + } + + rawHosts := make(map[string]string) + for host, prefix := range h { + rawHosts[string(host)] = prefix.String() + } + + return json.Marshal(rawHosts) +} + func (h Hosts) exist(name Host) bool { _, ok := h[name] return ok } +// MarshalJSON marshals the TagOwners to JSON. +func (to TagOwners) MarshalJSON() ([]byte, error) { + if to == nil { + return []byte("{}"), nil + } + + rawTagOwners := make(map[string][]string) + for tag, owners := range to { + tagStr := string(tag) + 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) + default: + return nil, fmt.Errorf("unknown owner type: %T", v) + } + } + + rawTagOwners[tagStr] = ownerStrs + } + + return json.Marshal(rawTagOwners) +} + // TagOwners are a map of Tag to a list of the UserEntities that own the tag. type TagOwners map[Tag]Owners @@ -855,8 +1061,32 @@ func resolveTagOwners(p *Policy, users types.Users, nodes types.Nodes) (map[Tag] } type AutoApproverPolicy struct { - Routes map[netip.Prefix]AutoApprovers `json:"routes"` - ExitNode AutoApprovers `json:"exitNode"` + Routes map[netip.Prefix]AutoApprovers `json:"routes,omitempty"` + ExitNode AutoApprovers `json:"exitNode,omitempty"` +} + +// MarshalJSON marshals the AutoApproverPolicy to JSON. +func (ap AutoApproverPolicy) MarshalJSON() ([]byte, error) { + // Marshal empty policies as empty object + if ap.Routes == nil && ap.ExitNode == nil { + return []byte("{}"), nil + } + + type Alias AutoApproverPolicy + + // Create a new object to avoid marshalling nil slices as null instead of empty arrays + obj := Alias(ap) + + // Initialize empty maps/slices to ensure they're marshalled as empty objects/arrays instead of null + if obj.Routes == nil { + obj.Routes = make(map[netip.Prefix]AutoApprovers) + } + + if obj.ExitNode == nil { + obj.ExitNode = AutoApprovers{} + } + + return json.Marshal(&obj) } // resolveAutoApprovers resolves the AutoApprovers to a map of netip.Prefix to netipx.IPSet. @@ -940,14 +1170,17 @@ type Policy struct { // callers using it should panic if not validated bool `json:"-"` - Groups Groups `json:"groups"` - Hosts Hosts `json:"hosts"` - TagOwners TagOwners `json:"tagOwners"` - ACLs []ACL `json:"acls"` - AutoApprovers AutoApproverPolicy `json:"autoApprovers"` - SSHs []SSH `json:"ssh"` + Groups Groups `json:"groups,omitempty"` + Hosts Hosts `json:"hosts,omitempty"` + TagOwners TagOwners `json:"tagOwners,omitempty"` + ACLs []ACL `json:"acls,omitempty"` + AutoApprovers AutoApproverPolicy `json:"autoApprovers,omitempty"` + SSHs []SSH `json:"ssh,omitempty"` } +// MarshalJSON is deliberately not implemented for Policy. +// We use the default JSON marshalling behavior provided by the Go runtime. + var ( autogroupForSrc = []AutoGroup{} autogroupForDst = []AutoGroup{AutoGroupInternet} @@ -1248,6 +1481,24 @@ type SSH struct { // It can be a list of usernames, groups, tags or autogroups. type SSHSrcAliases []Alias +// MarshalJSON marshals the Groups to JSON. +func (g Groups) MarshalJSON() ([]byte, error) { + if g == nil { + return []byte("{}"), nil + } + + raw := make(map[string][]string) + for group, usernames := range g { + users := make([]string, len(usernames)) + for i, username := range usernames { + users[i] = string(username) + } + raw[string(group)] = users + } + + return json.Marshal(raw) +} + func (a *SSHSrcAliases) UnmarshalJSON(b []byte) error { var aliases []AliasEnc err := json.Unmarshal(b, &aliases) @@ -1261,12 +1512,94 @@ func (a *SSHSrcAliases) UnmarshalJSON(b []byte) error { case *Username, *Group, *Tag, *AutoGroup: (*a)[i] = alias.Alias default: - return fmt.Errorf("type %T not supported", alias.Alias) + return fmt.Errorf( + "alias %T is not supported for SSH source", + alias.Alias, + ) } } return nil } +func (a *SSHDstAliases) UnmarshalJSON(b []byte) error { + var aliases []AliasEnc + err := json.Unmarshal(b, &aliases) + if err != nil { + return err + } + + *a = make([]Alias, len(aliases)) + for i, alias := range aliases { + switch alias.Alias.(type) { + case *Username, *Tag, *AutoGroup, *Host, + // Asterix and Group is actually not supposed to be supported, + // however we do not support autogroups at the moment + // so we will leave it in as there is no other option + // to dynamically give all access + // https://tailscale.com/kb/1193/tailscale-ssh#dst + // TODO(kradalby): remove this when we support autogroup:tagged and autogroup:member + Asterix: + (*a)[i] = alias.Alias + default: + return fmt.Errorf( + "alias %T is not supported for SSH destination", + alias.Alias, + ) + } + } + return nil +} + +// MarshalJSON marshals the SSHDstAliases to JSON. +func (a SSHDstAliases) MarshalJSON() ([]byte, error) { + if a == nil { + return []byte("[]"), nil + } + + 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) + default: + return nil, fmt.Errorf("unknown SSH destination alias type: %T", v) + } + } + + return json.Marshal(aliases) +} + +// MarshalJSON marshals the SSHSrcAliases to JSON. +func (a SSHSrcAliases) MarshalJSON() ([]byte, error) { + if a == nil { + return []byte("[]"), nil + } + + 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) + default: + return nil, fmt.Errorf("unknown SSH source alias type: %T", v) + } + } + + return json.Marshal(aliases) +} + func (a SSHSrcAliases) Resolve(p *Policy, users types.Users, nodes types.Nodes) (*netipx.IPSet, error) { var ips netipx.IPSetBuilder var errs []error @@ -1287,38 +1620,17 @@ func (a SSHSrcAliases) Resolve(p *Policy, users types.Users, nodes types.Nodes) // It can be a list of usernames, tags or autogroups. type SSHDstAliases []Alias -func (a *SSHDstAliases) UnmarshalJSON(b []byte) error { - var aliases []AliasEnc - err := json.Unmarshal(b, &aliases) - if err != nil { - return err - } - - *a = make([]Alias, len(aliases)) - for i, alias := range aliases { - switch alias.Alias.(type) { - case *Username, *Tag, *AutoGroup, - // Asterix and Group is actually not supposed to be supported, - // however we do not support autogroups at the moment - // so we will leave it in as there is no other option - // to dynamically give all access - // https://tailscale.com/kb/1193/tailscale-ssh#dst - // TODO(kradalby): remove this when we support autogroup:tagged and autogroup:member - Asterix: - (*a)[i] = alias.Alias - default: - return fmt.Errorf("type %T not supported", alias.Alias) - } - } - return nil -} - type SSHUser string func (u SSHUser) String() string { return string(u) } +// MarshalJSON marshals the SSHUser to JSON. +func (u SSHUser) MarshalJSON() ([]byte, error) { + return json.Marshal(string(u)) +} + // unmarshalPolicy takes a byte slice and unmarshals it into a Policy struct. // In addition to unmarshalling, it will also validate the policy. // This is the only entrypoint of reading a policy from a file or other source. diff --git a/hscontrol/policy/v2/types_test.go b/hscontrol/policy/v2/types_test.go index 3808b547..3e1d55a7 100644 --- a/hscontrol/policy/v2/types_test.go +++ b/hscontrol/policy/v2/types_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go4.org/netipx" xmaps "golang.org/x/exp/maps" @@ -19,6 +20,83 @@ import ( "tailscale.com/types/ptr" ) +// TestUnmarshalPolicy tests the unmarshalling of JSON into Policy objects and the marshalling +// back to JSON (round-trip). It ensures that: +// 1. JSON can be correctly unmarshalled into a Policy object +// 2. A Policy object can be correctly marshalled back to JSON +// 3. The unmarshalled Policy matches the expected Policy +// 4. The marshalled and then unmarshalled Policy is semantically equivalent to the original +// (accounting for nil vs empty map/slice differences) +// +// This test also verifies that all the required struct fields are properly marshalled and +// unmarshalled, maintaining semantic equivalence through a complete JSON round-trip. + +// TestMarshalJSON tests explicit marshalling of Policy objects to JSON. +// This test ensures our custom MarshalJSON methods properly encode +// the various data structures used in the Policy. +func TestMarshalJSON(t *testing.T) { + // Create a complex test policy + policy := &Policy{ + Groups: Groups{ + Group("group:example"): []Username{Username("user@example.com")}, + }, + Hosts: Hosts{ + "host-1": Prefix(mp("100.100.100.100/32")), + }, + TagOwners: TagOwners{ + Tag("tag:test"): Owners{up("user@example.com")}, + }, + ACLs: []ACL{ + { + Action: "accept", + Protocol: "tcp", + Sources: Aliases{ + ptr.To(Username("user@example.com")), + }, + Destinations: []AliasWithPorts{ + { + Alias: ptr.To(Username("other@example.com")), + Ports: []tailcfg.PortRange{{First: 80, Last: 80}}, + }, + }, + }, + }, + } + + // Marshal the policy to JSON + marshalled, err := json.MarshalIndent(policy, "", " ") + require.NoError(t, err) + + // Make sure all expected fields are present in the JSON + jsonString := string(marshalled) + assert.Contains(t, jsonString, "group:example") + assert.Contains(t, jsonString, "user@example.com") + assert.Contains(t, jsonString, "host-1") + assert.Contains(t, jsonString, "100.100.100.100/32") + assert.Contains(t, jsonString, "tag:test") + assert.Contains(t, jsonString, "accept") + assert.Contains(t, jsonString, "tcp") + assert.Contains(t, jsonString, "80") + + // Unmarshal back to verify round trip + var roundTripped Policy + err = json.Unmarshal(marshalled, &roundTripped) + require.NoError(t, err) + + // Compare the original and round-tripped policies + cmps := append(util.Comparers, cmp.Comparer(func(x, y Prefix) bool { + return x == y + })) + cmps = append(cmps, + cmpopts.IgnoreUnexported(Policy{}), + cmpopts.EquateEmpty(), + ) + + if diff := cmp.Diff(policy, &roundTripped, cmps...); diff != "" { + t.Fatalf("round trip policy (-original +roundtripped):\n%s", diff) + } +} + func TestUnmarshalPolicy(t *testing.T) { tests := []struct { name string @@ -712,25 +790,52 @@ func TestUnmarshalPolicy(t *testing.T) { return x == y })) cmps = append(cmps, cmpopts.IgnoreUnexported(Policy{})) + + // For round-trip testing, we'll normalize the policies before comparing for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Test unmarshalling policy, err := unmarshalPolicy([]byte(tt.input)) if tt.wantErr == "" { if err != nil { - t.Fatalf("got %v; want no error", err) + t.Fatalf("unmarshalling: got %v; want no error", err) } } else { if err == nil { - t.Fatalf("got nil; want error %q", tt.wantErr) + t.Fatalf("unmarshalling: got nil; want error %q", tt.wantErr) } else if !strings.Contains(err.Error(), tt.wantErr) { - t.Fatalf("got err %v; want error %q", err, tt.wantErr) + t.Fatalf("unmarshalling: got err %v; want error %q", err, tt.wantErr) } + return // Skip the rest of the test if we expected an error } if diff := cmp.Diff(tt.want, policy, cmps...); diff != "" { t.Fatalf("unexpected policy (-want +got):\n%s", diff) } + + // Test round-trip marshalling/unmarshalling + if policy != nil { + // Marshal the policy back to JSON + marshalled, err := json.MarshalIndent(policy, "", " ") + if err != nil { + t.Fatalf("marshalling: %v", err) + } + + // Unmarshal it again + roundTripped, err := unmarshalPolicy(marshalled) + if err != nil { + t.Fatalf("round-trip unmarshalling: %v", err) + } + + // Add EquateEmpty to handle nil vs empty maps/slices + roundTripCmps := append(cmps, cmpopts.EquateEmpty()) + + // Compare using the enhanced comparers for round-trip testing + if diff := cmp.Diff(policy, roundTripped, roundTripCmps...); diff != "" { + t.Fatalf("round trip policy (-original +roundtripped):\n%s", diff) + } + } }) } } diff --git a/integration/acl_test.go b/integration/acl_test.go index bb18b3b3..9181db13 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -7,50 +7,52 @@ import ( "testing" "github.com/google/go-cmp/cmp" - policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1" + "github.com/google/go-cmp/cmp/cmpopts" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "tailscale.com/tailcfg" ) -var veryLargeDestination = []string{ - "0.0.0.0/5:*", - "8.0.0.0/7:*", - "11.0.0.0/8:*", - "12.0.0.0/6:*", - "16.0.0.0/4:*", - "32.0.0.0/3:*", - "64.0.0.0/2:*", - "128.0.0.0/3:*", - "160.0.0.0/5:*", - "168.0.0.0/6:*", - "172.0.0.0/12:*", - "172.32.0.0/11:*", - "172.64.0.0/10:*", - "172.128.0.0/9:*", - "173.0.0.0/8:*", - "174.0.0.0/7:*", - "176.0.0.0/4:*", - "192.0.0.0/9:*", - "192.128.0.0/11:*", - "192.160.0.0/13:*", - "192.169.0.0/16:*", - "192.170.0.0/15:*", - "192.172.0.0/14:*", - "192.176.0.0/12:*", - "192.192.0.0/10:*", - "193.0.0.0/8:*", - "194.0.0.0/7:*", - "196.0.0.0/6:*", - "200.0.0.0/5:*", - "208.0.0.0/4:*", +var veryLargeDestination = []policyv2.AliasWithPorts{ + aliasWithPorts(prefixp("0.0.0.0/5"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("8.0.0.0/7"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("11.0.0.0/8"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("12.0.0.0/6"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("16.0.0.0/4"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("32.0.0.0/3"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("64.0.0.0/2"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("128.0.0.0/3"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("160.0.0.0/5"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("168.0.0.0/6"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("172.0.0.0/12"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("172.32.0.0/11"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("172.64.0.0/10"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("172.128.0.0/9"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("173.0.0.0/8"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("174.0.0.0/7"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("176.0.0.0/4"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.0.0.0/9"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.128.0.0/11"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.160.0.0/13"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.169.0.0/16"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.170.0.0/15"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.172.0.0/14"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.176.0.0/12"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("192.192.0.0/10"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("193.0.0.0/8"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("194.0.0.0/7"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("196.0.0.0/6"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("200.0.0.0/5"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("208.0.0.0/4"), tailcfg.PortRangeAny), } func aclScenario( t *testing.T, - policy *policyv1.ACLPolicy, + policy *policyv2.Policy, clientsPerUser int, ) *Scenario { t.Helper() @@ -108,19 +110,21 @@ func TestACLHostsInNetMapTable(t *testing.T) { // they can access minus one (them self). tests := map[string]struct { users ScenarioSpec - policy policyv1.ACLPolicy + policy policyv2.Policy want map[string]int }{ // Test that when we have no ACL, each client netmap has // the amount of peers of the total amount of clients "base-acls": { users: spec, - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, }, want: map[string]int{ @@ -133,17 +137,21 @@ func TestACLHostsInNetMapTable(t *testing.T) { // their own user. "two-isolated-users": { users: spec, - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user1@:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user1@"), tailcfg.PortRangeAny), + }, }, { - Action: "accept", - Sources: []string{"user2@"}, - Destinations: []string{"user2@:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user2@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRangeAny), + }, }, }, }, want: map[string]int{ @@ -156,27 +164,35 @@ func TestACLHostsInNetMapTable(t *testing.T) { // in the netmap. "two-restricted-present-in-netmap": { users: spec, - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user1@:22"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user1@"), tailcfg.PortRange{First: 22, Last: 22}), + }, }, { - Action: "accept", - Sources: []string{"user2@"}, - Destinations: []string{"user2@:22"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user2@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRange{First: 22, Last: 22}), + }, }, { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user2@:22"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRange{First: 22, Last: 22}), + }, }, { - Action: "accept", - Sources: []string{"user2@"}, - Destinations: []string{"user1@:22"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user2@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user1@"), tailcfg.PortRange{First: 22, Last: 22}), + }, }, }, }, want: map[string]int{ @@ -190,22 +206,28 @@ func TestACLHostsInNetMapTable(t *testing.T) { // need them present on the other side for the "return path". "two-ns-one-isolated": { users: spec, - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user1@:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user1@"), tailcfg.PortRangeAny), + }, }, { - Action: "accept", - Sources: []string{"user2@"}, - Destinations: []string{"user2@:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user2@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRangeAny), + }, }, { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user2@:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRangeAny), + }, }, }, }, want: map[string]int{ @@ -215,22 +237,37 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, "very-large-destination-prefix-1372": { users: spec, - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: append([]string{"user1@:*"}, veryLargeDestination...), + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: append( + []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user1@"), tailcfg.PortRangeAny), + }, + veryLargeDestination..., + ), }, { - Action: "accept", - Sources: []string{"user2@"}, - Destinations: append([]string{"user2@:*"}, veryLargeDestination...), + Action: "accept", + Sources: []policyv2.Alias{usernamep("user2@")}, + Destinations: append( + []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRangeAny), + }, + veryLargeDestination..., + ), }, { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: append([]string{"user2@:*"}, veryLargeDestination...), + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: append( + []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRangeAny), + }, + veryLargeDestination..., + ), }, }, }, want: map[string]int{ @@ -240,12 +277,15 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, "ipv6-acls-1470": { users: spec, - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"0.0.0.0/0:*", "::/0:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(prefixp("0.0.0.0/0"), tailcfg.PortRangeAny), + aliasWithPorts(prefixp("::/0"), tailcfg.PortRangeAny), + }, }, }, }, want: map[string]int{ @@ -295,12 +335,14 @@ func TestACLAllowUser80Dst(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user2@:80"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRange{First: 80, Last: 80}), + }, }, }, }, @@ -349,15 +391,17 @@ func TestACLDenyAllPort80(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &policyv1.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-acl-test": {"user1@", "user2@"}, + &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:integration-acl-test"): []policyv2.Username{policyv2.Username("user1@"), policyv2.Username("user2@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"group:integration-acl-test"}, - Destinations: []string{"*:22"}, + Action: "accept", + Sources: []policyv2.Alias{groupp("group:integration-acl-test")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRange{First: 22, Last: 22}), + }, }, }, }, @@ -396,12 +440,14 @@ func TestACLAllowUserDst(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user2@:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRangeAny), + }, }, }, }, @@ -452,12 +498,14 @@ func TestACLAllowStarDst(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, }, @@ -509,16 +557,18 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) { IntegrationSkip(t) scenario := aclScenario(t, - &policyv1.ACLPolicy{ - Hosts: policyv1.Hosts{ - "all": netip.MustParsePrefix("100.64.0.0/24"), + &policyv2.Policy{ + Hosts: policyv2.Hosts{ + "all": policyv2.Prefix(netip.MustParsePrefix("100.64.0.0/24")), }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ // Everyone can curl test3 { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"all:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(hostp("all"), tailcfg.PortRangeAny), + }, }, }, }, @@ -606,50 +656,58 @@ func TestACLNamedHostsCanReach(t *testing.T) { IntegrationSkip(t) tests := map[string]struct { - policy policyv1.ACLPolicy + policy policyv2.Policy }{ "ipv4": { - policy: policyv1.ACLPolicy{ - Hosts: policyv1.Hosts{ - "test1": netip.MustParsePrefix("100.64.0.1/32"), - "test2": netip.MustParsePrefix("100.64.0.2/32"), - "test3": netip.MustParsePrefix("100.64.0.3/32"), + policy: policyv2.Policy{ + Hosts: policyv2.Hosts{ + "test1": policyv2.Prefix(netip.MustParsePrefix("100.64.0.1/32")), + "test2": policyv2.Prefix(netip.MustParsePrefix("100.64.0.2/32")), + "test3": policyv2.Prefix(netip.MustParsePrefix("100.64.0.3/32")), }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ // Everyone can curl test3 { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"test3:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(hostp("test3"), tailcfg.PortRangeAny), + }, }, // test1 can curl test2 { - Action: "accept", - Sources: []string{"test1"}, - Destinations: []string{"test2:*"}, + Action: "accept", + Sources: []policyv2.Alias{hostp("test1")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(hostp("test2"), tailcfg.PortRangeAny), + }, }, }, }, }, "ipv6": { - policy: policyv1.ACLPolicy{ - Hosts: policyv1.Hosts{ - "test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"), - "test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"), - "test3": netip.MustParsePrefix("fd7a:115c:a1e0::3/128"), + policy: policyv2.Policy{ + Hosts: policyv2.Hosts{ + "test1": policyv2.Prefix(netip.MustParsePrefix("fd7a:115c:a1e0::1/128")), + "test2": policyv2.Prefix(netip.MustParsePrefix("fd7a:115c:a1e0::2/128")), + "test3": policyv2.Prefix(netip.MustParsePrefix("fd7a:115c:a1e0::3/128")), }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ // Everyone can curl test3 { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"test3:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(hostp("test3"), tailcfg.PortRangeAny), + }, }, // test1 can curl test2 { - Action: "accept", - Sources: []string{"test1"}, - Destinations: []string{"test2:*"}, + Action: "accept", + Sources: []policyv2.Alias{hostp("test1")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(hostp("test2"), tailcfg.PortRangeAny), + }, }, }, }, @@ -855,71 +913,81 @@ func TestACLDevice1CanAccessDevice2(t *testing.T) { IntegrationSkip(t) tests := map[string]struct { - policy policyv1.ACLPolicy + policy policyv2.Policy }{ "ipv4": { - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"100.64.0.1"}, - Destinations: []string{"100.64.0.2:*"}, + Action: "accept", + Sources: []policyv2.Alias{prefixp("100.64.0.1/32")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(prefixp("100.64.0.2/32"), tailcfg.PortRangeAny), + }, }, }, }, }, "ipv6": { - policy: policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"fd7a:115c:a1e0::1"}, - Destinations: []string{"fd7a:115c:a1e0::2:*"}, + Action: "accept", + Sources: []policyv2.Alias{prefixp("fd7a:115c:a1e0::1/128")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(prefixp("fd7a:115c:a1e0::2/128"), tailcfg.PortRangeAny), + }, }, }, }, }, "hostv4cidr": { - policy: policyv1.ACLPolicy{ - Hosts: policyv1.Hosts{ - "test1": netip.MustParsePrefix("100.64.0.1/32"), - "test2": netip.MustParsePrefix("100.64.0.2/32"), + policy: policyv2.Policy{ + Hosts: policyv2.Hosts{ + "test1": policyv2.Prefix(netip.MustParsePrefix("100.64.0.1/32")), + "test2": policyv2.Prefix(netip.MustParsePrefix("100.64.0.2/32")), }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"test1"}, - Destinations: []string{"test2:*"}, + Action: "accept", + Sources: []policyv2.Alias{hostp("test1")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(hostp("test2"), tailcfg.PortRangeAny), + }, }, }, }, }, "hostv6cidr": { - policy: policyv1.ACLPolicy{ - Hosts: policyv1.Hosts{ - "test1": netip.MustParsePrefix("fd7a:115c:a1e0::1/128"), - "test2": netip.MustParsePrefix("fd7a:115c:a1e0::2/128"), + policy: policyv2.Policy{ + Hosts: policyv2.Hosts{ + "test1": policyv2.Prefix(netip.MustParsePrefix("fd7a:115c:a1e0::1/128")), + "test2": policyv2.Prefix(netip.MustParsePrefix("fd7a:115c:a1e0::2/128")), }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"test1"}, - Destinations: []string{"test2:*"}, + Action: "accept", + Sources: []policyv2.Alias{hostp("test1")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(hostp("test2"), tailcfg.PortRangeAny), + }, }, }, }, }, "group": { - policy: policyv1.ACLPolicy{ - Groups: map[string][]string{ - "group:one": {"user1@"}, - "group:two": {"user2@"}, + policy: policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:one"): []policyv2.Username{policyv2.Username("user1@")}, + policyv2.Group("group:two"): []policyv2.Username{policyv2.Username("user2@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"group:one"}, - Destinations: []string{"group:two:*"}, + Action: "accept", + Sources: []policyv2.Alias{groupp("group:one")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(groupp("group:two"), tailcfg.PortRangeAny), + }, }, }, }, @@ -1073,15 +1141,17 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) { headscale, err := scenario.Headscale() require.NoError(t, err) - p := policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + p := policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"user1@"}, - Destinations: []string{"user2@:*"}, + Action: "accept", + Sources: []policyv2.Alias{usernamep("user1@")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(usernamep("user2@"), tailcfg.PortRangeAny), + }, }, }, - Hosts: policyv1.Hosts{}, + Hosts: policyv2.Hosts{}, } err = headscale.SetPolicy(&p) @@ -1089,7 +1159,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) { // Get the current policy and check // if it is the same as the one we set. - var output *policyv1.ACLPolicy + var output *policyv2.Policy err = executeAndUnmarshal( headscale, []string{ @@ -1105,7 +1175,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) { assert.Len(t, output.ACLs, 1) - if diff := cmp.Diff(p, *output); diff != "" { + if diff := cmp.Diff(p, *output, cmpopts.IgnoreUnexported(policyv2.Policy{}), cmpopts.EquateEmpty()); diff != "" { t.Errorf("unexpected policy(-want +got):\n%s", diff) } diff --git a/integration/cli_test.go b/integration/cli_test.go index 435b7e55..2cff0500 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -12,12 +12,13 @@ import ( tcmp "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "tailscale.com/tailcfg" "golang.org/x/exp/slices" ) @@ -912,13 +913,15 @@ func TestNodeTagCommand(t *testing.T) { ) } + + func TestNodeAdvertiseTagCommand(t *testing.T) { IntegrationSkip(t) t.Parallel() tests := []struct { name string - policy *policyv1.ACLPolicy + policy *policyv2.Policy wantTag bool }{ { @@ -927,51 +930,60 @@ func TestNodeAdvertiseTagCommand(t *testing.T) { }, { name: "with-policy-email", - policy: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - TagOwners: map[string][]string{ - "tag:test": {"user1@test.no"}, + TagOwners: policyv2.TagOwners{ + policyv2.Tag("tag:test"): policyv2.Owners{usernameOwner("user1@test.no")}, }, }, wantTag: true, }, { name: "with-policy-username", - policy: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + policy: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - TagOwners: map[string][]string{ - "tag:test": {"user1@"}, + TagOwners: policyv2.TagOwners{ + policyv2.Tag("tag:test"): policyv2.Owners{usernameOwner("user1@")}, }, }, wantTag: true, }, { name: "with-policy-groups", - policy: &policyv1.ACLPolicy{ - Groups: policyv1.Groups{ - "group:admins": []string{"user1@"}, + policy: &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:admins"): []policyv2.Username{policyv2.Username("user1@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - TagOwners: map[string][]string{ - "tag:test": {"group:admins"}, + TagOwners: policyv2.TagOwners{ + policyv2.Tag("tag:test"): policyv2.Owners{groupOwner("group:admins")}, }, }, wantTag: true, @@ -1746,16 +1758,19 @@ func TestPolicyCommand(t *testing.T) { headscale, err := scenario.Headscale() assertNoErr(t, err) - p := policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + p := policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - TagOwners: map[string][]string{ - "tag:exists": {"user1@"}, + TagOwners: policyv2.TagOwners{ + policyv2.Tag("tag:exists"): policyv2.Owners{usernameOwner("user1@")}, }, } @@ -1782,7 +1797,7 @@ func TestPolicyCommand(t *testing.T) { // Get the current policy and check // if it is the same as the one we set. - var output *policyv1.ACLPolicy + var output *policyv2.Policy err = executeAndUnmarshal( headscale, []string{ @@ -1825,18 +1840,21 @@ func TestPolicyBrokenConfigCommand(t *testing.T) { headscale, err := scenario.Headscale() assertNoErr(t, err) - p := policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + p := policyv2.Policy{ + ACLs: []policyv2.ACL{ { // This is an unknown action, so it will return an error // and the config will not be applied. - Action: "unknown-action", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "unknown-action", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - TagOwners: map[string][]string{ - "tag:exists": {"user1@"}, + TagOwners: policyv2.TagOwners{ + policyv2.Tag("tag:exists"): policyv2.Owners{usernameOwner("user1@")}, }, } diff --git a/integration/control.go b/integration/control.go index 22e7552b..df1d5d13 100644 --- a/integration/control.go +++ b/integration/control.go @@ -4,7 +4,7 @@ import ( "net/netip" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/ory/dockertest/v3" ) @@ -28,5 +28,5 @@ type ControlServer interface { ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error) GetCert() []byte GetHostname() string - SetPolicy(*policyv1.ACLPolicy) error + SetPolicy(*policyv2.Policy) error } diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index e6762cf0..35550c65 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -19,7 +19,7 @@ import ( "github.com/davecgh/go-spew/spew" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/dockertestutil" @@ -65,7 +65,7 @@ type HeadscaleInContainer struct { extraPorts []string caCerts [][]byte hostPortBindings map[string][]string - aclPolicy *policyv1.ACLPolicy + aclPolicy *policyv2.Policy env map[string]string tlsCert []byte tlsKey []byte @@ -80,7 +80,7 @@ type Option = func(c *HeadscaleInContainer) // WithACLPolicy adds a hscontrol.ACLPolicy policy to the // HeadscaleInContainer instance. -func WithACLPolicy(acl *policyv1.ACLPolicy) Option { +func WithACLPolicy(acl *policyv2.Policy) Option { return func(hsic *HeadscaleInContainer) { if acl == nil { return @@ -188,13 +188,6 @@ func WithPostgres() Option { } } -// WithPolicyV1 tells the integration test to use the old v1 filter. -func WithPolicyV1() Option { - return func(hsic *HeadscaleInContainer) { - hsic.env["HEADSCALE_POLICY_V1"] = "1" - } -} - // WithPolicy sets the policy mode for headscale func WithPolicyMode(mode types.PolicyMode) Option { return func(hsic *HeadscaleInContainer) { @@ -889,7 +882,7 @@ func (t *HeadscaleInContainer) MapUsers() (map[string]*v1.User, error) { return userMap, nil } -func (h *HeadscaleInContainer) SetPolicy(pol *policyv1.ACLPolicy) error { +func (h *HeadscaleInContainer) SetPolicy(pol *policyv2.Policy) error { err := h.writePolicy(pol) if err != nil { return fmt.Errorf("writing policy file: %w", err) @@ -930,7 +923,7 @@ func (h *HeadscaleInContainer) reloadDatabasePolicy() error { return nil } -func (h *HeadscaleInContainer) writePolicy(pol *policyv1.ACLPolicy) error { +func (h *HeadscaleInContainer) writePolicy(pol *policyv2.Policy) error { pBytes, err := json.Marshal(pol) if err != nil { return fmt.Errorf("marshalling pol: %w", err) diff --git a/integration/route_test.go b/integration/route_test.go index 5a85f436..ba3bf288 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/netip" "sort" + "strings" "testing" "time" @@ -13,7 +14,7 @@ import ( cmpdiff "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/hsic" @@ -22,6 +23,7 @@ import ( "github.com/stretchr/testify/require" "tailscale.com/ipn/ipnstate" "tailscale.com/net/tsaddr" + "tailscale.com/tailcfg" "tailscale.com/types/ipproto" "tailscale.com/types/views" "tailscale.com/util/must" @@ -793,25 +795,31 @@ func TestSubnetRouteACL(t *testing.T) { err = scenario.CreateHeadscaleEnv([]tsic.Option{ tsic.WithAcceptRoutes(), }, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy( - &policyv1.ACLPolicy{ - Groups: policyv1.Groups{ - "group:admins": {user + "@"}, + &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:admins"): []policyv2.Username{policyv2.Username(user + "@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"group:admins"}, - Destinations: []string{"group:admins:*"}, + Action: "accept", + Sources: []policyv2.Alias{groupp("group:admins")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(groupp("group:admins"), tailcfg.PortRangeAny), + }, }, { - Action: "accept", - Sources: []string{"group:admins"}, - Destinations: []string{"10.33.0.0/16:*"}, + Action: "accept", + Sources: []policyv2.Alias{groupp("group:admins")}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(prefixp("10.33.0.0/16"), tailcfg.PortRangeAny), + }, }, // { - // Action: "accept", - // Sources: []string{"group:admins"}, - // Destinations: []string{"0.0.0.0/0:*"}, + // Action: "accept", + // Sources: []policyv2.Alias{groupp("group:admins")}, + // Destinations: []policyv2.AliasWithPorts{ + // aliasWithPorts(prefixp("0.0.0.0/0"), tailcfg.PortRangeAny), + // }, // }, }, }, @@ -1384,29 +1392,31 @@ func TestAutoApproveMultiNetwork(t *testing.T) { tests := []struct { name string - pol *policyv1.ACLPolicy + pol *policyv2.Policy approver string spec ScenarioSpec withURL bool }{ { name: "authkey-tag", - pol: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + pol: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - TagOwners: map[string][]string{ - "tag:approve": {"user1@"}, + TagOwners: policyv2.TagOwners{ + policyv2.Tag("tag:approve"): policyv2.Owners{usernameOwner("user1@")}, }, - AutoApprovers: policyv1.AutoApprovers{ - Routes: map[string][]string{ - bigRoute.String(): {"tag:approve"}, + AutoApprovers: policyv2.AutoApproverPolicy{ + Routes: map[netip.Prefix]policyv2.AutoApprovers{ + bigRoute: {tagApprover("tag:approve")}, }, - ExitNode: []string{"tag:approve"}, + ExitNode: policyv2.AutoApprovers{tagApprover("tag:approve")}, }, }, approver: "tag:approve", @@ -1427,19 +1437,21 @@ func TestAutoApproveMultiNetwork(t *testing.T) { }, { name: "authkey-user", - pol: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + pol: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - AutoApprovers: policyv1.AutoApprovers{ - Routes: map[string][]string{ - bigRoute.String(): {"user1@"}, + AutoApprovers: policyv2.AutoApproverPolicy{ + Routes: map[netip.Prefix]policyv2.AutoApprovers{ + bigRoute: {usernameApprover("user1@")}, }, - ExitNode: []string{"user1@"}, + ExitNode: policyv2.AutoApprovers{usernameApprover("user1@")}, }, }, approver: "user1@", @@ -1460,22 +1472,24 @@ func TestAutoApproveMultiNetwork(t *testing.T) { }, { name: "authkey-group", - pol: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + pol: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - Groups: policyv1.Groups{ - "group:approve": []string{"user1@"}, + Groups: policyv2.Groups{ + policyv2.Group("group:approve"): []policyv2.Username{policyv2.Username("user1@")}, }, - AutoApprovers: policyv1.AutoApprovers{ - Routes: map[string][]string{ - bigRoute.String(): {"group:approve"}, + AutoApprovers: policyv2.AutoApproverPolicy{ + Routes: map[netip.Prefix]policyv2.AutoApprovers{ + bigRoute: {groupApprover("group:approve")}, }, - ExitNode: []string{"group:approve"}, + ExitNode: policyv2.AutoApprovers{groupApprover("group:approve")}, }, }, approver: "group:approve", @@ -1496,19 +1510,21 @@ func TestAutoApproveMultiNetwork(t *testing.T) { }, { name: "webauth-user", - pol: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + pol: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - AutoApprovers: policyv1.AutoApprovers{ - Routes: map[string][]string{ - bigRoute.String(): {"user1@"}, + AutoApprovers: policyv2.AutoApproverPolicy{ + Routes: map[netip.Prefix]policyv2.AutoApprovers{ + bigRoute: {usernameApprover("user1@")}, }, - ExitNode: []string{"user1@"}, + ExitNode: policyv2.AutoApprovers{usernameApprover("user1@")}, }, }, approver: "user1@", @@ -1530,22 +1546,24 @@ func TestAutoApproveMultiNetwork(t *testing.T) { }, { name: "webauth-tag", - pol: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + pol: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - TagOwners: map[string][]string{ - "tag:approve": {"user1@"}, + TagOwners: policyv2.TagOwners{ + policyv2.Tag("tag:approve"): policyv2.Owners{usernameOwner("user1@")}, }, - AutoApprovers: policyv1.AutoApprovers{ - Routes: map[string][]string{ - bigRoute.String(): {"tag:approve"}, + AutoApprovers: policyv2.AutoApproverPolicy{ + Routes: map[netip.Prefix]policyv2.AutoApprovers{ + bigRoute: {tagApprover("tag:approve")}, }, - ExitNode: []string{"tag:approve"}, + ExitNode: policyv2.AutoApprovers{tagApprover("tag:approve")}, }, }, approver: "tag:approve", @@ -1567,22 +1585,24 @@ func TestAutoApproveMultiNetwork(t *testing.T) { }, { name: "webauth-group", - pol: &policyv1.ACLPolicy{ - ACLs: []policyv1.ACL{ + pol: &policyv2.Policy{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - Groups: policyv1.Groups{ - "group:approve": []string{"user1@"}, + Groups: policyv2.Groups{ + policyv2.Group("group:approve"): []policyv2.Username{policyv2.Username("user1@")}, }, - AutoApprovers: policyv1.AutoApprovers{ - Routes: map[string][]string{ - bigRoute.String(): {"group:approve"}, + AutoApprovers: policyv2.AutoApproverPolicy{ + Routes: map[netip.Prefix]policyv2.AutoApprovers{ + bigRoute: {groupApprover("group:approve")}, }, - ExitNode: []string{"group:approve"}, + ExitNode: policyv2.AutoApprovers{groupApprover("group:approve")}, }, }, approver: "group:approve", @@ -1657,7 +1677,20 @@ func TestAutoApproveMultiNetwork(t *testing.T) { assert.NotNil(t, headscale) // Set the route of usernet1 to be autoapproved - tt.pol.AutoApprovers.Routes[route.String()] = []string{tt.approver} + var approvers policyv2.AutoApprovers + switch { + case strings.HasPrefix(tt.approver, "tag:"): + approvers = append(approvers, tagApprover(tt.approver)) + case strings.HasPrefix(tt.approver, "group:"): + approvers = append(approvers, groupApprover(tt.approver)) + default: + approvers = append(approvers, usernameApprover(tt.approver)) + } + if tt.pol.AutoApprovers.Routes == nil { + tt.pol.AutoApprovers.Routes = make(map[netip.Prefix]policyv2.AutoApprovers) + } + prefix := *route + tt.pol.AutoApprovers.Routes[prefix] = approvers err = headscale.SetPolicy(tt.pol) require.NoError(t, err) @@ -1767,7 +1800,8 @@ func TestAutoApproveMultiNetwork(t *testing.T) { assertTracerouteViaIP(t, tr, routerUsernet1.MustIPv4()) // Remove the auto approval from the policy, any routes already enabled should be allowed. - delete(tt.pol.AutoApprovers.Routes, route.String()) + prefix = *route + delete(tt.pol.AutoApprovers.Routes, prefix) err = headscale.SetPolicy(tt.pol) require.NoError(t, err) @@ -1831,7 +1865,20 @@ func TestAutoApproveMultiNetwork(t *testing.T) { // Add the route back to the auto approver in the policy, the route should // now become available again. - tt.pol.AutoApprovers.Routes[route.String()] = []string{tt.approver} + var newApprovers policyv2.AutoApprovers + switch { + case strings.HasPrefix(tt.approver, "tag:"): + newApprovers = append(newApprovers, tagApprover(tt.approver)) + case strings.HasPrefix(tt.approver, "group:"): + newApprovers = append(newApprovers, groupApprover(tt.approver)) + default: + newApprovers = append(newApprovers, usernameApprover(tt.approver)) + } + if tt.pol.AutoApprovers.Routes == nil { + tt.pol.AutoApprovers.Routes = make(map[netip.Prefix]policyv2.AutoApprovers) + } + prefix = *route + tt.pol.AutoApprovers.Routes[prefix] = newApprovers err = headscale.SetPolicy(tt.pol) require.NoError(t, err) @@ -2070,7 +2117,9 @@ func TestSubnetRouteACLFiltering(t *testing.T) { "src": [ "node" ], - "dst": [] + "dst": [ + "*:*" + ] } ] }`) @@ -2090,8 +2139,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) { weburl := fmt.Sprintf("http://%s/etc/hostname", webip) t.Logf("webservice: %s, %s", webip.String(), weburl) - // Create ACL policy - aclPolicy := &policyv1.ACLPolicy{} + aclPolicy := &policyv2.Policy{} err = json.Unmarshal([]byte(aclPolicyStr), aclPolicy) require.NoError(t, err) @@ -2121,24 +2169,23 @@ func TestSubnetRouteACLFiltering(t *testing.T) { routerClient := allClients[0] nodeClient := allClients[1] - aclPolicy.Hosts = policyv1.Hosts{ - routerUser: must.Get(routerClient.MustIPv4().Prefix(32)), - nodeUser: must.Get(nodeClient.MustIPv4().Prefix(32)), + aclPolicy.Hosts = policyv2.Hosts{ + policyv2.Host(routerUser): policyv2.Prefix(must.Get(routerClient.MustIPv4().Prefix(32))), + policyv2.Host(nodeUser): policyv2.Prefix(must.Get(nodeClient.MustIPv4().Prefix(32))), } - aclPolicy.ACLs[1].Destinations = []string{ - route.String() + ":*", + aclPolicy.ACLs[1].Destinations = []policyv2.AliasWithPorts{ + aliasWithPorts(prefixp(route.String()), tailcfg.PortRangeAny), } - require.NoError(t, headscale.SetPolicy(aclPolicy)) // Set up the subnet routes for the router - routes := []string{ - route.String(), // This should be accessible by the client - "10.10.11.0/24", // These should NOT be accessible - "10.10.12.0/24", + routes := []netip.Prefix{ + *route, // This should be accessible by the client + netip.MustParsePrefix("10.10.11.0/24"), // These should NOT be accessible + netip.MustParsePrefix("10.10.12.0/24"), } - routeArg := "--advertise-routes=" + routes[0] + "," + routes[1] + "," + routes[2] + routeArg := "--advertise-routes=" + routes[0].String() + "," + routes[1].String() + "," + routes[2].String() command := []string{ "tailscale", "set", @@ -2208,5 +2255,4 @@ func TestSubnetRouteACLFiltering(t *testing.T) { tr, err := nodeClient.Traceroute(webip) require.NoError(t, err) assertTracerouteViaIP(t, tr, routerClient.MustIPv4()) - } diff --git a/integration/scenario.go b/integration/scenario.go index 7d4d62d1..507c248d 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -47,7 +47,6 @@ const ( ) var usePostgresForTest = envknob.Bool("HEADSCALE_INTEGRATION_POSTGRES") -var usePolicyV1ForTest = envknob.Bool("HEADSCALE_POLICY_V1") var ( errNoHeadscaleAvailable = errors.New("no headscale available") @@ -414,10 +413,6 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { opts = append(opts, hsic.WithPostgres()) } - if usePolicyV1ForTest { - opts = append(opts, hsic.WithPolicyV1()) - } - headscale, err := hsic.New(s.pool, s.Networks(), opts...) if err != nil { return nil, fmt.Errorf("failed to create headscale container: %w", err) diff --git a/integration/ssh_test.go b/integration/ssh_test.go index 25ede0c4..0bbd8711 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -7,10 +7,11 @@ import ( "testing" "time" - policyv1 "github.com/juanfont/headscale/hscontrol/policy/v1" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" + "tailscale.com/tailcfg" ) func isSSHNoAccessStdError(stderr string) bool { @@ -48,7 +49,7 @@ var retry = func(times int, sleepInterval time.Duration, return result, stderr, err } -func sshScenario(t *testing.T, policy *policyv1.ACLPolicy, clientsPerUser int) *Scenario { +func sshScenario(t *testing.T, policy *policyv2.Policy, clientsPerUser int) *Scenario { t.Helper() spec := ScenarioSpec{ @@ -92,23 +93,26 @@ func TestSSHOneUserToAll(t *testing.T) { t.Parallel() scenario := sshScenario(t, - &policyv1.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1@"}, + &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:integration-test"): []policyv2.Username{policyv2.Username("user1@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - SSHs: []policyv1.SSH{ + SSHs: []policyv2.SSH{ { Action: "accept", - Sources: []string{"group:integration-test"}, - Destinations: []string{"*"}, - Users: []string{"ssh-it-user"}, + Sources: policyv2.SSHSrcAliases{groupp("group:integration-test")}, + Destinations: policyv2.SSHDstAliases{wildcard()}, + Users: []policyv2.SSHUser{policyv2.SSHUser("ssh-it-user")}, }, }, }, @@ -157,23 +161,26 @@ func TestSSHMultipleUsersAllToAll(t *testing.T) { t.Parallel() scenario := sshScenario(t, - &policyv1.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1@", "user2@"}, + &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:integration-test"): []policyv2.Username{policyv2.Username("user1@"), policyv2.Username("user2@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - SSHs: []policyv1.SSH{ + SSHs: []policyv2.SSH{ { Action: "accept", - Sources: []string{"group:integration-test"}, - Destinations: []string{"user1@", "user2@"}, - Users: []string{"ssh-it-user"}, + Sources: policyv2.SSHSrcAliases{groupp("group:integration-test")}, + Destinations: policyv2.SSHDstAliases{usernamep("user1@"), usernamep("user2@")}, + Users: []policyv2.SSHUser{policyv2.SSHUser("ssh-it-user")}, }, }, }, @@ -210,18 +217,21 @@ func TestSSHNoSSHConfigured(t *testing.T) { t.Parallel() scenario := sshScenario(t, - &policyv1.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1@"}, + &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:integration-test"): []policyv2.Username{policyv2.Username("user1@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - SSHs: []policyv1.SSH{}, + SSHs: []policyv2.SSH{}, }, len(MustTestVersions), ) @@ -252,23 +262,26 @@ func TestSSHIsBlockedInACL(t *testing.T) { t.Parallel() scenario := sshScenario(t, - &policyv1.ACLPolicy{ - Groups: map[string][]string{ - "group:integration-test": {"user1@"}, + &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:integration-test"): []policyv2.Username{policyv2.Username("user1@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:80"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRange{First: 80, Last: 80}), + }, }, }, - SSHs: []policyv1.SSH{ + SSHs: []policyv2.SSH{ { Action: "accept", - Sources: []string{"group:integration-test"}, - Destinations: []string{"user1@"}, - Users: []string{"ssh-it-user"}, + Sources: policyv2.SSHSrcAliases{groupp("group:integration-test")}, + Destinations: policyv2.SSHDstAliases{usernamep("user1@")}, + Users: []policyv2.SSHUser{policyv2.SSHUser("ssh-it-user")}, }, }, }, @@ -301,30 +314,33 @@ func TestSSHUserOnlyIsolation(t *testing.T) { t.Parallel() scenario := sshScenario(t, - &policyv1.ACLPolicy{ - Groups: map[string][]string{ - "group:ssh1": {"user1@"}, - "group:ssh2": {"user2@"}, + &policyv2.Policy{ + Groups: policyv2.Groups{ + policyv2.Group("group:ssh1"): []policyv2.Username{policyv2.Username("user1@")}, + policyv2.Group("group:ssh2"): []policyv2.Username{policyv2.Username("user2@")}, }, - ACLs: []policyv1.ACL{ + ACLs: []policyv2.ACL{ { - Action: "accept", - Sources: []string{"*"}, - Destinations: []string{"*:*"}, + Action: "accept", + Protocol: "tcp", + Sources: []policyv2.Alias{wildcard()}, + Destinations: []policyv2.AliasWithPorts{ + aliasWithPorts(wildcard(), tailcfg.PortRangeAny), + }, }, }, - SSHs: []policyv1.SSH{ + SSHs: []policyv2.SSH{ { Action: "accept", - Sources: []string{"group:ssh1"}, - Destinations: []string{"user1@"}, - Users: []string{"ssh-it-user"}, + Sources: policyv2.SSHSrcAliases{groupp("group:ssh1")}, + Destinations: policyv2.SSHDstAliases{usernamep("user1@")}, + Users: []policyv2.SSHUser{policyv2.SSHUser("ssh-it-user")}, }, { Action: "accept", - Sources: []string{"group:ssh2"}, - Destinations: []string{"user2@"}, - Users: []string{"ssh-it-user"}, + Sources: policyv2.SSHSrcAliases{groupp("group:ssh2")}, + Destinations: policyv2.SSHDstAliases{usernamep("user2@")}, + Users: []policyv2.SSHUser{policyv2.SSHUser("ssh-it-user")}, }, }, }, diff --git a/integration/utils.go b/integration/utils.go index 440fa663..18721cad 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -5,15 +5,19 @@ import ( "bytes" "fmt" "io" + "net/netip" "strings" "sync" "testing" "time" "github.com/cenkalti/backoff/v4" + policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" + "tailscale.com/tailcfg" + "tailscale.com/types/ptr" ) const ( @@ -419,10 +423,76 @@ func countMatchingLines(in io.Reader, predicate func(string) bool) (int, error) // return peer // } // } -// } +// } // // return nil // } + +// Helper functions for creating typed policy entities + +// wildcard returns a wildcard alias (*). +func wildcard() policyv2.Alias { + return policyv2.Wildcard +} + +// usernamep returns a pointer to a Username as an Alias. +func usernamep(name string) policyv2.Alias { + return ptr.To(policyv2.Username(name)) +} + +// hostp returns a pointer to a Host. +func hostp(name string) policyv2.Alias { + return ptr.To(policyv2.Host(name)) +} + +// groupp returns a pointer to a Group as an Alias. +func groupp(name string) policyv2.Alias { + return ptr.To(policyv2.Group(name)) +} + +// tagp returns a pointer to a Tag as an Alias. +func tagp(name string) policyv2.Alias { + return ptr.To(policyv2.Tag(name)) +} + +// prefixp returns a pointer to a Prefix from a CIDR string. +func prefixp(cidr string) policyv2.Alias { + prefix := netip.MustParsePrefix(cidr) + return ptr.To(policyv2.Prefix(prefix)) +} + +// aliasWithPorts creates an AliasWithPorts structure from an alias and ports. +func aliasWithPorts(alias policyv2.Alias, ports ...tailcfg.PortRange) policyv2.AliasWithPorts { + return policyv2.AliasWithPorts{ + Alias: alias, + Ports: ports, + } +} + +// usernameOwner returns a Username as an Owner for use in TagOwners. +func usernameOwner(name string) policyv2.Owner { + return ptr.To(policyv2.Username(name)) +} + +// groupOwner returns a Group as an Owner for use in TagOwners. +func groupOwner(name string) policyv2.Owner { + return ptr.To(policyv2.Group(name)) +} + +// usernameApprover returns a Username as an AutoApprover. +func usernameApprover(name string) policyv2.AutoApprover { + return ptr.To(policyv2.Username(name)) +} + +// groupApprover returns a Group as an AutoApprover. +func groupApprover(name string) policyv2.AutoApprover { + return ptr.To(policyv2.Group(name)) +} + +// tagApprover returns a Tag as an AutoApprover. +func tagApprover(name string) policyv2.AutoApprover { + return ptr.To(policyv2.Tag(name)) +} // // // findPeerByHostname takes a hostname and a map of peers from status.Peer, and returns a *ipnstate.PeerStatus // // if there is a peer with the given hostname. If no peer is found, nil is returned.