mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-26 08:53:05 +00:00
2068 AutoApprovers tests (#2105)
* replace old suite approved routes test with table driven Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * add test to reproduce issue Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> * add integration test for 2068 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com> --------- Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
adc084f20f
commit
f368ed01ed
5 changed files with 215 additions and 70 deletions
1
.github/workflows/test-integration.yaml
vendored
1
.github/workflows/test-integration.yaml
vendored
|
@ -55,6 +55,7 @@ jobs:
|
||||||
- TestEnablingRoutes
|
- TestEnablingRoutes
|
||||||
- TestHASubnetRouterFailover
|
- TestHASubnetRouterFailover
|
||||||
- TestEnableDisableAutoApprovedRoute
|
- TestEnableDisableAutoApprovedRoute
|
||||||
|
- TestAutoApprovedSubRoute2068
|
||||||
- TestSubnetRouteACL
|
- TestSubnetRouteACL
|
||||||
- TestHeadscale
|
- TestHeadscale
|
||||||
- TestCreateTailscale
|
- TestCreateTailscale
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -518,8 +519,37 @@ func TestHeadscale_generateGivenName(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestAutoApproveRoutes(c *check.C) {
|
func TestAutoApproveRoutes(t *testing.T) {
|
||||||
acl := []byte(`
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
acl string
|
||||||
|
routes []netip.Prefix
|
||||||
|
want []netip.Prefix
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "2068-approve-issue-sub",
|
||||||
|
acl: `
|
||||||
|
{
|
||||||
|
"groups": {
|
||||||
|
"group:k8s": ["test"]
|
||||||
|
},
|
||||||
|
|
||||||
|
"acls": [
|
||||||
|
{"action": "accept", "users": ["*"], "ports": ["*:*"]},
|
||||||
|
],
|
||||||
|
|
||||||
|
"autoApprovers": {
|
||||||
|
"routes": {
|
||||||
|
"10.42.0.0/16": ["test"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
routes: []netip.Prefix{netip.MustParsePrefix("10.42.7.0/24")},
|
||||||
|
want: []netip.Prefix{netip.MustParsePrefix("10.42.7.0/24")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "2068-approve-issue-sub",
|
||||||
|
acl: `
|
||||||
{
|
{
|
||||||
"tagOwners": {
|
"tagOwners": {
|
||||||
"tag:exit": ["test"],
|
"tag:exit": ["test"],
|
||||||
|
@ -540,28 +570,40 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
|
||||||
"10.11.0.0/16": ["test"],
|
"10.11.0.0/16": ["test"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}`,
|
||||||
`)
|
routes: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
netip.MustParsePrefix("::/0"),
|
||||||
|
netip.MustParsePrefix("10.10.0.0/16"),
|
||||||
|
netip.MustParsePrefix("10.11.0.0/24"),
|
||||||
|
},
|
||||||
|
want: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("::/0"),
|
||||||
|
netip.MustParsePrefix("10.11.0.0/24"),
|
||||||
|
netip.MustParsePrefix("10.10.0.0/16"),
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
pol, err := policy.LoadACLPolicyFromBytes(acl)
|
for _, tt := range tests {
|
||||||
c.Assert(err, check.IsNil)
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
c.Assert(pol, check.NotNil)
|
adb, err := newTestDB()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
pol, err := policy.LoadACLPolicyFromBytes([]byte(tt.acl))
|
||||||
|
|
||||||
user, err := db.CreateUser("test")
|
assert.NoError(t, err)
|
||||||
c.Assert(err, check.IsNil)
|
assert.NotNil(t, pol)
|
||||||
|
|
||||||
pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
user, err := adb.CreateUser("test")
|
||||||
c.Assert(err, check.IsNil)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
pak, err := adb.CreatePreAuthKey(user.Name, false, false, nil, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
nodeKey := key.NewNode()
|
nodeKey := key.NewNode()
|
||||||
machineKey := key.NewMachine()
|
machineKey := key.NewMachine()
|
||||||
|
|
||||||
defaultRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
|
||||||
defaultRouteV6 := netip.MustParsePrefix("::/0")
|
|
||||||
route1 := netip.MustParsePrefix("10.10.0.0/16")
|
|
||||||
// Check if a subprefix of an autoapproved route is approved
|
|
||||||
route2 := netip.MustParsePrefix("10.11.0.0/24")
|
|
||||||
|
|
||||||
v4 := netip.MustParseAddr("100.64.0.1")
|
v4 := netip.MustParseAddr("100.64.0.1")
|
||||||
node := types.Node{
|
node := types.Node{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
@ -573,28 +615,38 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) {
|
||||||
AuthKeyID: ptr.To(pak.ID),
|
AuthKeyID: ptr.To(pak.ID),
|
||||||
Hostinfo: &tailcfg.Hostinfo{
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
RequestTags: []string{"tag:exit"},
|
RequestTags: []string{"tag:exit"},
|
||||||
RoutableIPs: []netip.Prefix{defaultRouteV4, defaultRouteV6, route1, route2},
|
RoutableIPs: tt.routes,
|
||||||
},
|
},
|
||||||
IPv4: &v4,
|
IPv4: &v4,
|
||||||
}
|
}
|
||||||
|
|
||||||
trx := db.DB.Save(&node)
|
trx := adb.DB.Save(&node)
|
||||||
c.Assert(trx.Error, check.IsNil)
|
assert.NoError(t, trx.Error)
|
||||||
|
|
||||||
sendUpdate, err := db.SaveNodeRoutes(&node)
|
sendUpdate, err := adb.SaveNodeRoutes(&node)
|
||||||
c.Assert(err, check.IsNil)
|
assert.NoError(t, err)
|
||||||
c.Assert(sendUpdate, check.Equals, false)
|
assert.False(t, sendUpdate)
|
||||||
|
|
||||||
node0ByID, err := db.GetNodeByID(0)
|
node0ByID, err := adb.GetNodeByID(0)
|
||||||
c.Assert(err, check.IsNil)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// TODO(kradalby): Check state update
|
// TODO(kradalby): Check state update
|
||||||
err = db.EnableAutoApprovedRoutes(pol, node0ByID)
|
err = adb.EnableAutoApprovedRoutes(pol, node0ByID)
|
||||||
c.Assert(err, check.IsNil)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
enabledRoutes, err := db.GetEnabledRoutes(node0ByID)
|
enabledRoutes, err := adb.GetEnabledRoutes(node0ByID)
|
||||||
c.Assert(err, check.IsNil)
|
assert.NoError(t, err)
|
||||||
c.Assert(enabledRoutes, check.HasLen, 4)
|
assert.Len(t, enabledRoutes, len(tt.want))
|
||||||
|
|
||||||
|
sort.Slice(enabledRoutes, func(i, j int) bool {
|
||||||
|
return util.ComparePrefix(enabledRoutes[i], enabledRoutes[j]) > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.want, enabledRoutes, util.Comparers...); diff != "" {
|
||||||
|
t.Errorf("unexpected enabled routes (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEphemeralGarbageCollectorOrder(t *testing.T) {
|
func TestEphemeralGarbageCollectorOrder(t *testing.T) {
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package hscontrol
|
package hscontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cmp"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -14,6 +12,7 @@ import (
|
||||||
"github.com/juanfont/headscale/hscontrol/db"
|
"github.com/juanfont/headscale/hscontrol/db"
|
||||||
"github.com/juanfont/headscale/hscontrol/mapper"
|
"github.com/juanfont/headscale/hscontrol/mapper"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/sasha-s/go-deadlock"
|
"github.com/sasha-s/go-deadlock"
|
||||||
xslices "golang.org/x/exp/slices"
|
xslices "golang.org/x/exp/slices"
|
||||||
|
@ -742,10 +741,10 @@ func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) {
|
||||||
newRoutes := new.RoutableIPs
|
newRoutes := new.RoutableIPs
|
||||||
|
|
||||||
sort.Slice(oldRoutes, func(i, j int) bool {
|
sort.Slice(oldRoutes, func(i, j int) bool {
|
||||||
return comparePrefix(oldRoutes[i], oldRoutes[j]) > 0
|
return util.ComparePrefix(oldRoutes[i], oldRoutes[j]) > 0
|
||||||
})
|
})
|
||||||
sort.Slice(newRoutes, func(i, j int) bool {
|
sort.Slice(newRoutes, func(i, j int) bool {
|
||||||
return comparePrefix(newRoutes[i], newRoutes[j]) > 0
|
return util.ComparePrefix(newRoutes[i], newRoutes[j]) > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
if !xslices.Equal(oldRoutes, newRoutes) {
|
if !xslices.Equal(oldRoutes, newRoutes) {
|
||||||
|
@ -764,19 +763,3 @@ func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) {
|
||||||
|
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(kradalby): Remove after go 1.23, will be in stdlib.
|
|
||||||
// Compare returns an integer comparing two prefixes.
|
|
||||||
// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2.
|
|
||||||
// Prefixes sort first by validity (invalid before valid), then
|
|
||||||
// address family (IPv4 before IPv6), then prefix length, then
|
|
||||||
// address.
|
|
||||||
func comparePrefix(p, p2 netip.Prefix) int {
|
|
||||||
if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
return p.Addr().Compare(p2.Addr())
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
|
func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
@ -10,3 +12,20 @@ func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
|
||||||
|
|
||||||
return d.DialContext(ctx, "unix", addr)
|
return d.DialContext(ctx, "unix", addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO(kradalby): Remove after go 1.24, will be in stdlib.
|
||||||
|
// Compare returns an integer comparing two prefixes.
|
||||||
|
// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2.
|
||||||
|
// Prefixes sort first by validity (invalid before valid), then
|
||||||
|
// address family (IPv4 before IPv6), then prefix length, then
|
||||||
|
// address.
|
||||||
|
func ComparePrefix(p, p2 netip.Prefix) int {
|
||||||
|
if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return p.Addr().Compare(p2.Addr())
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
||||||
"github.com/juanfont/headscale/hscontrol/policy"
|
"github.com/juanfont/headscale/hscontrol/policy"
|
||||||
"github.com/juanfont/headscale/hscontrol/util"
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
@ -957,6 +958,95 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) {
|
||||||
assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary())
|
assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAutoApprovedSubRoute2068(t *testing.T) {
|
||||||
|
IntegrationSkip(t)
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
expectedRoutes := "10.42.7.0/24"
|
||||||
|
|
||||||
|
user := "subroute"
|
||||||
|
|
||||||
|
scenario, err := NewScenario(dockertestMaxWait())
|
||||||
|
assertNoErrf(t, "failed to create scenario: %s", err)
|
||||||
|
defer scenario.Shutdown()
|
||||||
|
|
||||||
|
spec := map[string]int{
|
||||||
|
user: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:approve"})}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
|
||||||
|
&policy.ACLPolicy{
|
||||||
|
ACLs: []policy.ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: []string{"*"},
|
||||||
|
Destinations: []string{"*:*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TagOwners: map[string][]string{
|
||||||
|
"tag:approve": {user},
|
||||||
|
},
|
||||||
|
AutoApprovers: policy.AutoApprovers{
|
||||||
|
Routes: map[string][]string{
|
||||||
|
"10.42.0.0/16": {"tag:approve"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
))
|
||||||
|
assertNoErrHeadscaleEnv(t, err)
|
||||||
|
|
||||||
|
allClients, err := scenario.ListTailscaleClients()
|
||||||
|
assertNoErrListClients(t, err)
|
||||||
|
|
||||||
|
err = scenario.WaitForTailscaleSync()
|
||||||
|
assertNoErrSync(t, err)
|
||||||
|
|
||||||
|
headscale, err := scenario.Headscale()
|
||||||
|
assertNoErrGetHeadscale(t, err)
|
||||||
|
|
||||||
|
subRouter1 := allClients[0]
|
||||||
|
|
||||||
|
// Initially advertise route
|
||||||
|
command := []string{
|
||||||
|
"tailscale",
|
||||||
|
"set",
|
||||||
|
"--advertise-routes=" + expectedRoutes,
|
||||||
|
}
|
||||||
|
_, _, err = subRouter1.Execute(command)
|
||||||
|
assertNoErrf(t, "failed to advertise route: %s", err)
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
|
var routes []*v1.Route
|
||||||
|
err = executeAndUnmarshal(
|
||||||
|
headscale,
|
||||||
|
[]string{
|
||||||
|
"headscale",
|
||||||
|
"routes",
|
||||||
|
"list",
|
||||||
|
"--output",
|
||||||
|
"json",
|
||||||
|
},
|
||||||
|
&routes,
|
||||||
|
)
|
||||||
|
assertNoErr(t, err)
|
||||||
|
assert.Len(t, routes, 1)
|
||||||
|
|
||||||
|
want := []*v1.Route{
|
||||||
|
{
|
||||||
|
Id: 1,
|
||||||
|
Prefix: expectedRoutes,
|
||||||
|
Advertised: true,
|
||||||
|
Enabled: true,
|
||||||
|
IsPrimary: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(want, routes, cmpopts.IgnoreUnexported(v1.Route{}), cmpopts.IgnoreFields(v1.Route{}, "Node", "CreatedAt", "UpdatedAt", "DeletedAt")); diff != "" {
|
||||||
|
t.Errorf("unexpected routes (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestSubnetRouteACL verifies that Subnet routes are distributed
|
// TestSubnetRouteACL verifies that Subnet routes are distributed
|
||||||
// as expected when ACLs are activated.
|
// as expected when ACLs are activated.
|
||||||
// It implements the issue from
|
// It implements the issue from
|
||||||
|
|
Loading…
Reference in a new issue