2022-10-24 16:41:40 +02:00
|
|
|
package integration
|
|
|
|
|
|
|
|
|
|
import (
|
2024-12-10 16:23:55 +01:00
|
|
|
"cmp"
|
2022-10-24 16:41:40 +02:00
|
|
|
"encoding/json"
|
2022-11-28 15:54:23 +00:00
|
|
|
"fmt"
|
2025-12-02 12:01:25 +01:00
|
|
|
"slices"
|
2025-04-30 12:45:08 +03:00
|
|
|
"strconv"
|
2024-09-07 09:23:58 +02:00
|
|
|
"strings"
|
2022-10-24 16:41:40 +02:00
|
|
|
"testing"
|
2022-10-24 16:42:09 +02:00
|
|
|
"time"
|
2022-10-24 16:41:40 +02:00
|
|
|
|
2024-12-10 16:23:55 +01:00
|
|
|
tcmp "github.com/google/go-cmp/cmp"
|
|
|
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
2022-10-24 16:41:40 +02:00
|
|
|
v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
|
2025-05-20 13:57:26 +02:00
|
|
|
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
2025-01-26 22:20:11 +01:00
|
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
2022-11-14 15:01:31 +01:00
|
|
|
"github.com/juanfont/headscale/integration/hsic"
|
2022-11-08 15:10:03 +00:00
|
|
|
"github.com/juanfont/headscale/integration/tsic"
|
2024-07-22 08:56:00 +02:00
|
|
|
"github.com/stretchr/testify/assert"
|
2024-12-10 16:23:55 +01:00
|
|
|
"github.com/stretchr/testify/require"
|
2025-07-10 23:38:55 +02:00
|
|
|
"tailscale.com/tailcfg"
|
2022-10-24 16:41:40 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func executeAndUnmarshal[T any](headscale ControlServer, command []string, result T) error {
|
|
|
|
|
str, err := headscale.Execute(command)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = json.Unmarshal([]byte(str), result)
|
|
|
|
|
if err != nil {
|
2025-07-10 23:38:55 +02:00
|
|
|
return fmt.Errorf("failed to unmarshal: %w\n command err: %s", err, str)
|
2022-10-24 16:41:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-10 16:23:55 +01:00
|
|
|
// Interface ensuring that we can sort structs from gRPC that
|
|
|
|
|
// have an ID field.
|
|
|
|
|
type GRPCSortable interface {
|
|
|
|
|
GetId() uint64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func sortWithID[T GRPCSortable](a, b T) int {
|
|
|
|
|
return cmp.Compare(a.GetId(), b.GetId())
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
func TestUserCommand(t *testing.T) {
|
2022-10-24 16:41:40 +02:00
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"user1", "user2"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2022-10-24 16:41:40 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clins"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-10-24 16:41:40 +02:00
|
|
|
|
2022-11-14 14:27:02 +01:00
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-11-14 14:27:02 +01:00
|
|
|
|
2024-12-10 16:23:55 +01:00
|
|
|
var listUsers []*v1.User
|
2025-07-13 17:37:11 +02:00
|
|
|
var result []string
|
|
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
err := executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listUsers,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(ct, err)
|
2022-10-24 16:41:40 +02:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
slices.SortFunc(listUsers, sortWithID)
|
|
|
|
|
result = []string{listUsers[0].GetName(), listUsers[1].GetName()}
|
2022-10-24 17:03:59 +02:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.Equal(
|
|
|
|
|
ct,
|
|
|
|
|
[]string{"user1", "user2"},
|
|
|
|
|
result,
|
|
|
|
|
"Should have user1 and user2 in users list",
|
|
|
|
|
)
|
|
|
|
|
}, 20*time.Second, 1*time.Second)
|
2022-10-24 16:41:40 +02:00
|
|
|
|
2022-11-14 14:27:02 +01:00
|
|
|
_, err = headscale.Execute(
|
2022-10-24 16:41:40 +02:00
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
2023-01-17 17:43:44 +01:00
|
|
|
"users",
|
2022-10-24 16:41:40 +02:00
|
|
|
"rename",
|
2024-12-10 16:23:55 +01:00
|
|
|
"--output=json",
|
|
|
|
|
fmt.Sprintf("--identifier=%d", listUsers[1].GetId()),
|
|
|
|
|
"--new-name=newname",
|
2022-10-24 16:41:40 +02:00
|
|
|
},
|
|
|
|
|
)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-10-24 16:41:40 +02:00
|
|
|
|
2024-12-10 16:23:55 +01:00
|
|
|
var listAfterRenameUsers []*v1.User
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
err := executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAfterRenameUsers,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(ct, err)
|
2022-10-24 16:41:40 +02:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
slices.SortFunc(listAfterRenameUsers, sortWithID)
|
|
|
|
|
result = []string{listAfterRenameUsers[0].GetName(), listAfterRenameUsers[1].GetName()}
|
2022-10-24 17:03:59 +02:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.Equal(
|
|
|
|
|
ct,
|
|
|
|
|
[]string{"user1", "newname"},
|
|
|
|
|
result,
|
|
|
|
|
"Should have user1 and newname after rename operation",
|
|
|
|
|
)
|
|
|
|
|
}, 20*time.Second, 1*time.Second)
|
2024-12-10 16:23:55 +01:00
|
|
|
|
|
|
|
|
var listByUsername []*v1.User
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
"--name=user1",
|
|
|
|
|
},
|
|
|
|
|
&listByUsername,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for user list by username")
|
2024-12-10 16:23:55 +01:00
|
|
|
|
|
|
|
|
slices.SortFunc(listByUsername, sortWithID)
|
|
|
|
|
want := []*v1.User{
|
|
|
|
|
{
|
2024-12-19 13:10:10 +01:00
|
|
|
Id: 1,
|
|
|
|
|
Name: "user1",
|
|
|
|
|
Email: "user1@test.no",
|
2024-12-10 16:23:55 +01:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if diff := tcmp.Diff(want, listByUsername, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
|
|
|
|
t.Errorf("unexpected users (-want +got):\n%s", diff)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var listByID []*v1.User
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
"--identifier=1",
|
|
|
|
|
},
|
|
|
|
|
&listByID,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for user list by ID")
|
2024-12-10 16:23:55 +01:00
|
|
|
|
|
|
|
|
slices.SortFunc(listByID, sortWithID)
|
|
|
|
|
want = []*v1.User{
|
|
|
|
|
{
|
2024-12-19 13:10:10 +01:00
|
|
|
Id: 1,
|
|
|
|
|
Name: "user1",
|
|
|
|
|
Email: "user1@test.no",
|
2024-12-10 16:23:55 +01:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if diff := tcmp.Diff(want, listByID, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
|
|
|
|
t.Errorf("unexpected users (-want +got):\n%s", diff)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deleteResult, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"destroy",
|
|
|
|
|
"--force",
|
|
|
|
|
// Delete "user1"
|
|
|
|
|
"--identifier=1",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2024-12-10 16:23:55 +01:00
|
|
|
assert.Contains(t, deleteResult, "User destroyed")
|
|
|
|
|
|
|
|
|
|
var listAfterIDDelete []*v1.User
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
err := executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAfterIDDelete,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(ct, err)
|
|
|
|
|
|
|
|
|
|
slices.SortFunc(listAfterIDDelete, sortWithID)
|
|
|
|
|
want := []*v1.User{
|
|
|
|
|
{
|
|
|
|
|
Id: 2,
|
|
|
|
|
Name: "newname",
|
|
|
|
|
Email: "user2@test.no",
|
|
|
|
|
},
|
|
|
|
|
}
|
2024-12-10 16:23:55 +01:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
if diff := tcmp.Diff(want, listAfterIDDelete, cmpopts.IgnoreUnexported(v1.User{}), cmpopts.IgnoreFields(v1.User{}, "CreatedAt")); diff != "" {
|
|
|
|
|
assert.Fail(ct, "unexpected users", "diff (-want +got):\n%s", diff)
|
|
|
|
|
}
|
|
|
|
|
}, 20*time.Second, 1*time.Second)
|
2024-12-10 16:23:55 +01:00
|
|
|
|
|
|
|
|
deleteResult, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"destroy",
|
|
|
|
|
"--force",
|
|
|
|
|
"--name=newname",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2024-12-10 16:23:55 +01:00
|
|
|
assert.Contains(t, deleteResult, "User destroyed")
|
|
|
|
|
|
|
|
|
|
var listAfterNameDelete []v1.User
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"users",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAfterNameDelete,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
assert.Empty(c, listAfterNameDelete)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for user list after name delete")
|
2022-10-24 16:41:40 +02:00
|
|
|
}
|
2022-10-24 16:42:09 +02:00
|
|
|
|
|
|
|
|
func TestPreAuthKeyCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
user := "preauthkeyspace"
|
2022-10-24 16:42:09 +02:00
|
|
|
count := 3
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{user},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2022-10-24 16:42:09 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clipak"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-10-24 16:42:09 +02:00
|
|
|
|
2022-11-14 14:27:02 +01:00
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-11-14 14:27:02 +01:00
|
|
|
|
2022-10-24 16:42:09 +02:00
|
|
|
keys := make([]*v1.PreAuthKey, count)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-10-24 16:42:09 +02:00
|
|
|
|
2025-03-30 13:19:05 +02:00
|
|
|
for index := range count {
|
2022-10-24 16:42:09 +02:00
|
|
|
var preAuthKey v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err := executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
"1",
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable",
|
|
|
|
|
"--expiration",
|
|
|
|
|
"24h",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
"--tags",
|
|
|
|
|
"tag:test1,tag:test2",
|
|
|
|
|
},
|
|
|
|
|
&preAuthKey,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for preauth key creation")
|
|
|
|
|
|
|
|
|
|
keys[index] = &preAuthKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.Len(t, keys, 3)
|
|
|
|
|
|
|
|
|
|
var listedPreAuthKeys []v1.PreAuthKey
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
2022-11-14 14:27:02 +01:00
|
|
|
headscale,
|
2022-10-24 16:42:09 +02:00
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
2023-01-17 17:43:44 +01:00
|
|
|
"--user",
|
2025-04-30 12:45:08 +03:00
|
|
|
"1",
|
2025-10-23 17:57:41 +02:00
|
|
|
"list",
|
2022-10-24 16:42:09 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
2025-10-23 17:57:41 +02:00
|
|
|
&listedPreAuthKeys,
|
2022-10-24 16:42:09 +02:00
|
|
|
)
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for preauth keys list")
|
2022-10-24 16:42:09 +02:00
|
|
|
|
|
|
|
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
|
|
|
|
assert.Len(t, listedPreAuthKeys, 4)
|
|
|
|
|
|
|
|
|
|
assert.Equal(
|
|
|
|
|
t,
|
2025-04-30 12:45:08 +03:00
|
|
|
[]uint64{keys[0].GetId(), keys[1].GetId(), keys[2].GetId()},
|
|
|
|
|
[]uint64{
|
2023-12-09 18:09:24 +01:00
|
|
|
listedPreAuthKeys[1].GetId(),
|
|
|
|
|
listedPreAuthKeys[2].GetId(),
|
|
|
|
|
listedPreAuthKeys[3].GetId(),
|
|
|
|
|
},
|
2022-10-24 16:42:09 +02:00
|
|
|
)
|
|
|
|
|
|
2025-11-12 09:36:36 -06:00
|
|
|
// New keys show prefix after listing, so check the created keys instead
|
|
|
|
|
assert.NotEmpty(t, keys[0].GetKey())
|
|
|
|
|
assert.NotEmpty(t, keys[1].GetKey())
|
|
|
|
|
assert.NotEmpty(t, keys[2].GetKey())
|
2022-10-24 16:42:09 +02:00
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
|
|
|
|
|
assert.True(t, listedPreAuthKeys[2].GetExpiration().AsTime().After(time.Now()))
|
|
|
|
|
assert.True(t, listedPreAuthKeys[3].GetExpiration().AsTime().After(time.Now()))
|
2022-10-24 16:42:09 +02:00
|
|
|
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2022-10-24 16:42:09 +02:00
|
|
|
)
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedPreAuthKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2022-10-24 16:42:09 +02:00
|
|
|
)
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedPreAuthKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2022-10-24 16:42:09 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for index := range listedPreAuthKeys {
|
|
|
|
|
if index == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-23 16:03:58 +02:00
|
|
|
assert.Equal(
|
|
|
|
|
t,
|
|
|
|
|
[]string{"tag:test1", "tag:test2"},
|
|
|
|
|
listedPreAuthKeys[index].GetAclTags(),
|
|
|
|
|
)
|
2022-10-24 16:42:09 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-12 09:36:36 -06:00
|
|
|
// Test key expiry - use the full key from creation, not the masked one from listing
|
2022-11-14 14:27:02 +01:00
|
|
|
_, err = headscale.Execute(
|
2022-10-24 16:42:09 +02:00
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
2023-01-17 17:43:44 +01:00
|
|
|
"--user",
|
2025-04-30 12:45:08 +03:00
|
|
|
"1",
|
2022-10-24 16:42:09 +02:00
|
|
|
"expire",
|
2025-11-12 09:36:36 -06:00
|
|
|
keys[0].GetKey(),
|
2022-10-24 16:42:09 +02:00
|
|
|
},
|
|
|
|
|
)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-10-24 16:42:09 +02:00
|
|
|
|
|
|
|
|
var listedPreAuthKeysAfterExpire []v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
"1",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listedPreAuthKeysAfterExpire,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for preauth keys list after expire")
|
2022-10-24 16:42:09 +02:00
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.True(t, listedPreAuthKeysAfterExpire[1].GetExpiration().AsTime().Before(time.Now()))
|
|
|
|
|
assert.True(t, listedPreAuthKeysAfterExpire[2].GetExpiration().AsTime().After(time.Now()))
|
|
|
|
|
assert.True(t, listedPreAuthKeysAfterExpire[3].GetExpiration().AsTime().After(time.Now()))
|
2022-10-24 16:42:09 +02:00
|
|
|
}
|
2022-10-24 17:31:15 +02:00
|
|
|
|
|
|
|
|
func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
user := "pre-auth-key-without-exp-user"
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{user},
|
|
|
|
|
}
|
2022-10-24 17:31:15 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2022-10-24 17:31:15 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clipaknaexp"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-10-24 17:31:15 +02:00
|
|
|
|
2022-11-14 14:27:02 +01:00
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-11-14 14:27:02 +01:00
|
|
|
|
2022-10-24 17:31:15 +02:00
|
|
|
var preAuthKey v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
"1",
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&preAuthKey,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for preauth key creation without expiry")
|
2022-10-24 17:31:15 +02:00
|
|
|
|
|
|
|
|
var listedPreAuthKeys []v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
"1",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listedPreAuthKeys,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for preauth keys list without expiry")
|
2022-10-24 17:31:15 +02:00
|
|
|
|
|
|
|
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
|
|
|
|
assert.Len(t, listedPreAuthKeys, 2)
|
|
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.True(t, listedPreAuthKeys[1].GetExpiration().AsTime().After(time.Now()))
|
2022-10-24 17:31:15 +02:00
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedPreAuthKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Minute*70)),
|
2022-10-24 17:31:15 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2023-01-17 17:43:44 +01:00
|
|
|
user := "pre-auth-key-reus-ephm-user"
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{user},
|
|
|
|
|
}
|
2022-10-24 17:31:15 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2022-10-24 17:31:15 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clipakresueeph"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-10-24 17:31:15 +02:00
|
|
|
|
2022-11-14 14:27:02 +01:00
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2022-11-14 14:27:02 +01:00
|
|
|
|
2022-10-24 17:31:15 +02:00
|
|
|
var preAuthReusableKey v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
"1",
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable=true",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&preAuthReusableKey,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for reusable preauth key creation")
|
2022-10-24 17:31:15 +02:00
|
|
|
|
|
|
|
|
var preAuthEphemeralKey v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
"1",
|
|
|
|
|
"create",
|
|
|
|
|
"--ephemeral=true",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&preAuthEphemeralKey,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for ephemeral preauth key creation")
|
2022-10-24 17:31:15 +02:00
|
|
|
|
|
|
|
|
assert.True(t, preAuthEphemeralKey.GetEphemeral())
|
|
|
|
|
assert.False(t, preAuthEphemeralKey.GetReusable())
|
|
|
|
|
|
|
|
|
|
var listedPreAuthKeys []v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
"1",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listedPreAuthKeys,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for preauth keys list after reusable/ephemeral creation")
|
2022-10-24 17:31:15 +02:00
|
|
|
|
|
|
|
|
// There is one key created by "scenario.CreateHeadscaleEnv"
|
|
|
|
|
assert.Len(t, listedPreAuthKeys, 3)
|
|
|
|
|
}
|
2022-11-28 15:54:23 +00:00
|
|
|
|
2024-05-02 11:53:16 +02:00
|
|
|
func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
|
|
|
|
user1 := "user1"
|
|
|
|
|
user2 := "user2"
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
NodesPerUser: 1,
|
|
|
|
|
Users: []string{user1},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
2024-10-15 15:38:43 +03:00
|
|
|
err = scenario.CreateHeadscaleEnv(
|
|
|
|
|
[]tsic.Option{},
|
|
|
|
|
hsic.WithTestName("clipak"),
|
|
|
|
|
hsic.WithEmbeddedDERPServerOnly(),
|
|
|
|
|
hsic.WithTLS(),
|
|
|
|
|
)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
2025-04-30 12:45:08 +03:00
|
|
|
u2, err := headscale.CreateUser(user2)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2025-03-21 11:49:32 +01:00
|
|
|
|
2024-05-02 11:53:16 +02:00
|
|
|
var user2Key v1.PreAuthKey
|
|
|
|
|
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user",
|
|
|
|
|
strconv.FormatUint(u2.GetId(), 10),
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable",
|
|
|
|
|
"--expiration",
|
|
|
|
|
"24h",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
"--tags",
|
|
|
|
|
"tag:test1,tag:test2",
|
|
|
|
|
},
|
|
|
|
|
&user2Key,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for user2 preauth key creation")
|
2024-05-02 11:53:16 +02:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
var listNodes []*v1.Node
|
|
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
var err error
|
|
|
|
|
listNodes, err = headscale.ListNodes()
|
|
|
|
|
assert.NoError(ct, err)
|
|
|
|
|
assert.Len(ct, listNodes, 1, "Should have exactly 1 node for user1")
|
|
|
|
|
assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "Node should belong to user1")
|
|
|
|
|
}, 15*time.Second, 1*time.Second)
|
2025-03-21 11:49:32 +01:00
|
|
|
|
2024-05-02 11:53:16 +02:00
|
|
|
allClients, err := scenario.ListTailscaleClients()
|
2025-10-16 12:17:43 +02:00
|
|
|
requireNoErrListClients(t, err)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
require.Len(t, allClients, 1)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
|
|
|
|
client := allClients[0]
|
|
|
|
|
|
|
|
|
|
// Log out from user1
|
|
|
|
|
err = client.Logout()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
|
|
|
|
err = scenario.WaitForTailscaleLogout()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
status, err := client.Status()
|
|
|
|
|
assert.NoError(ct, err)
|
2025-07-28 11:15:53 +02:00
|
|
|
assert.NotContains(ct, []string{"Starting", "Running"}, status.BackendState,
|
2025-07-13 17:37:11 +02:00
|
|
|
"Expected node to be logged out, backend state: %s", status.BackendState)
|
|
|
|
|
}, 30*time.Second, 2*time.Second)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
|
|
|
|
err = client.Login(headscale.GetEndpoint(), user2Key.GetKey())
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-05-02 11:53:16 +02:00
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
status, err := client.Status()
|
|
|
|
|
assert.NoError(ct, err)
|
|
|
|
|
assert.Equal(ct, "Running", status.BackendState, "Expected node to be logged in, backend state: %s", status.BackendState)
|
2025-12-02 12:01:25 +01:00
|
|
|
// With tags-as-identity model, tagged nodes show as TaggedDevices user (2147455555)
|
|
|
|
|
// The PreAuthKey was created with tags, so the node is tagged
|
|
|
|
|
assert.Equal(ct, "userid:2147455555", status.Self.UserID.String(), "Expected node to be logged in as tagged-devices user")
|
2025-07-13 17:37:11 +02:00
|
|
|
}, 30*time.Second, 2*time.Second)
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
var err error
|
|
|
|
|
listNodes, err = headscale.ListNodes()
|
|
|
|
|
assert.NoError(ct, err)
|
|
|
|
|
assert.Len(ct, listNodes, 2, "Should have 2 nodes after re-login")
|
|
|
|
|
assert.Equal(ct, user1, listNodes[0].GetUser().GetName(), "First node should belong to user1")
|
2025-12-02 12:01:25 +01:00
|
|
|
// Second node is tagged (created with tagged PreAuthKey), so it shows as "tagged-devices"
|
|
|
|
|
assert.Equal(ct, "tagged-devices", listNodes[1].GetUser().GetName(), "Second node should be tagged-devices")
|
2025-07-13 17:37:11 +02:00
|
|
|
}, 20*time.Second, 1*time.Second)
|
2024-05-02 11:53:16 +02:00
|
|
|
}
|
|
|
|
|
|
2023-05-10 12:10:33 +02:00
|
|
|
func TestApiKeyCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
|
|
|
|
count := 5
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"user1", "user2"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clins"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
keys := make([]string, count)
|
|
|
|
|
|
2025-03-30 13:19:05 +02:00
|
|
|
for idx := range count {
|
2023-05-10 12:10:33 +02:00
|
|
|
apiResult, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"apikeys",
|
|
|
|
|
"create",
|
|
|
|
|
"--expiration",
|
|
|
|
|
"24h",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
assert.NotEmpty(t, apiResult)
|
|
|
|
|
|
|
|
|
|
keys[idx] = apiResult
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.Len(t, keys, 5)
|
|
|
|
|
|
|
|
|
|
var listedAPIKeys []v1.ApiKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"apikeys",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listedAPIKeys,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for API keys list")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Len(t, listedAPIKeys, 5)
|
|
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.Equal(t, uint64(1), listedAPIKeys[0].GetId())
|
|
|
|
|
assert.Equal(t, uint64(2), listedAPIKeys[1].GetId())
|
|
|
|
|
assert.Equal(t, uint64(3), listedAPIKeys[2].GetId())
|
|
|
|
|
assert.Equal(t, uint64(4), listedAPIKeys[3].GetId())
|
|
|
|
|
assert.Equal(t, uint64(5), listedAPIKeys[4].GetId())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.NotEmpty(t, listedAPIKeys[0].GetPrefix())
|
|
|
|
|
assert.NotEmpty(t, listedAPIKeys[1].GetPrefix())
|
|
|
|
|
assert.NotEmpty(t, listedAPIKeys[2].GetPrefix())
|
|
|
|
|
assert.NotEmpty(t, listedAPIKeys[3].GetPrefix())
|
|
|
|
|
assert.NotEmpty(t, listedAPIKeys[4].GetPrefix())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.True(t, listedAPIKeys[0].GetExpiration().AsTime().After(time.Now()))
|
|
|
|
|
assert.True(t, listedAPIKeys[1].GetExpiration().AsTime().After(time.Now()))
|
|
|
|
|
assert.True(t, listedAPIKeys[2].GetExpiration().AsTime().After(time.Now()))
|
|
|
|
|
assert.True(t, listedAPIKeys[3].GetExpiration().AsTime().After(time.Now()))
|
|
|
|
|
assert.True(t, listedAPIKeys[4].GetExpiration().AsTime().After(time.Now()))
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAPIKeys[0].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAPIKeys[1].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAPIKeys[2].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAPIKeys[3].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAPIKeys[4].GetExpiration().AsTime().Before(time.Now().Add(time.Hour*26)),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expiredPrefixes := make(map[string]bool)
|
|
|
|
|
|
|
|
|
|
// Expire three keys
|
2025-03-30 13:19:05 +02:00
|
|
|
for idx := range 3 {
|
2023-05-10 12:10:33 +02:00
|
|
|
_, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"apikeys",
|
|
|
|
|
"expire",
|
|
|
|
|
"--prefix",
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAPIKeys[idx].GetPrefix(),
|
2023-05-10 12:10:33 +02:00
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var listedAfterExpireAPIKeys []v1.ApiKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"apikeys",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listedAfterExpireAPIKeys,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for API keys list after expire")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
for index := range listedAfterExpireAPIKeys {
|
2023-11-23 08:31:33 +01:00
|
|
|
if _, ok := expiredPrefixes[listedAfterExpireAPIKeys[index].GetPrefix()]; ok {
|
2023-05-10 12:10:33 +02:00
|
|
|
// Expired
|
|
|
|
|
assert.True(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
// Not expired
|
|
|
|
|
assert.False(
|
|
|
|
|
t,
|
2023-11-23 08:31:33 +01:00
|
|
|
listedAfterExpireAPIKeys[index].GetExpiration().AsTime().Before(time.Now()),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-12 16:01:21 +05:30
|
|
|
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"apikeys",
|
|
|
|
|
"delete",
|
|
|
|
|
"--prefix",
|
|
|
|
|
listedAPIKeys[0].GetPrefix(),
|
|
|
|
|
})
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2024-02-12 16:01:21 +05:30
|
|
|
|
|
|
|
|
var listedAPIKeysAfterDelete []v1.ApiKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"apikeys",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listedAPIKeysAfterDelete,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for API keys list after delete")
|
2024-02-12 16:01:21 +05:30
|
|
|
|
|
|
|
|
assert.Len(t, listedAPIKeysAfterDelete, 4)
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNodeTagCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"user1"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clins"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// Test 1: Verify that tags require authorization via ACL policy
|
|
|
|
|
// The tags-as-identity model allows conversion from user-owned to tagged, but only
|
|
|
|
|
// if the tag is authorized via tagOwners in the ACL policy.
|
|
|
|
|
regID := types.MustRegistrationID().String()
|
|
|
|
|
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"debug",
|
|
|
|
|
"create-node",
|
|
|
|
|
"--name",
|
|
|
|
|
"user-owned-node",
|
|
|
|
|
"--user",
|
|
|
|
|
"user1",
|
|
|
|
|
"--key",
|
|
|
|
|
regID,
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
var userOwnedNode v1.Node
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
2023-05-10 12:10:33 +02:00
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
2025-12-02 12:01:25 +01:00
|
|
|
"nodes",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--user",
|
|
|
|
|
"user1",
|
2025-12-02 12:01:25 +01:00
|
|
|
"register",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--key",
|
2025-01-26 22:20:11 +01:00
|
|
|
regID,
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
2025-12-02 12:01:25 +01:00
|
|
|
&userOwnedNode,
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
2025-12-02 12:01:25 +01:00
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for user-owned node registration")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// Verify node is user-owned (no tags)
|
|
|
|
|
assert.Empty(t, userOwnedNode.GetValidTags(), "User-owned node should not have tags")
|
|
|
|
|
assert.Empty(t, userOwnedNode.GetForcedTags(), "User-owned node should not have forced tags")
|
|
|
|
|
|
|
|
|
|
// Attempt to set tags on user-owned node should FAIL because there's no ACL policy
|
|
|
|
|
// authorizing the tag. The tags-as-identity model allows conversion from user-owned
|
|
|
|
|
// to tagged, but only if the tag is authorized via tagOwners in the ACL policy.
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"tag",
|
|
|
|
|
"-i", strconv.FormatUint(userOwnedNode.GetId(), 10),
|
|
|
|
|
"-t", "tag:test",
|
|
|
|
|
"--output", "json",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
require.ErrorContains(t, err, "invalid or unauthorized tags", "Setting unauthorized tags should fail")
|
|
|
|
|
|
|
|
|
|
// Test 2: Verify tag format validation
|
|
|
|
|
// Create a PreAuthKey with tags to create a tagged node
|
|
|
|
|
// Get the user ID from the node
|
|
|
|
|
userID := userOwnedNode.GetUser().GetId()
|
|
|
|
|
|
|
|
|
|
var preAuthKey v1.PreAuthKey
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user", strconv.FormatUint(userID, 10),
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable",
|
|
|
|
|
"--tags", "tag:integration-test",
|
|
|
|
|
"--output", "json",
|
|
|
|
|
},
|
|
|
|
|
&preAuthKey,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Creating PreAuthKey with tags")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// Verify PreAuthKey has tags
|
|
|
|
|
assert.Contains(t, preAuthKey.GetAclTags(), "tag:integration-test", "PreAuthKey should have tags")
|
|
|
|
|
|
|
|
|
|
// Test 3: Verify invalid tag format is rejected
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user", strconv.FormatUint(userID, 10),
|
|
|
|
|
"create",
|
|
|
|
|
"--tags", "wrong-tag", // Missing "tag:" prefix
|
|
|
|
|
"--output", "json",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert.ErrorContains(t, err, "tag must start with the string 'tag:'", "Invalid tag format should be rejected")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestTaggedNodeRegistration(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
|
|
|
|
// ACL policy that authorizes the tags used in tagged PreAuthKeys
|
|
|
|
|
// user1 and user2 can assign these tags when creating PreAuthKeys
|
|
|
|
|
policy := &policyv2.Policy{
|
|
|
|
|
TagOwners: policyv2.TagOwners{
|
|
|
|
|
"tag:server": policyv2.Owners{usernameOwner("user1@"), usernameOwner("user2@")},
|
|
|
|
|
"tag:prod": policyv2.Owners{usernameOwner("user1@"), usernameOwner("user2@")},
|
|
|
|
|
"tag:forbidden": policyv2.Owners{usernameOwner("user1@"), usernameOwner("user2@")},
|
|
|
|
|
},
|
|
|
|
|
ACLs: []policyv2.ACL{
|
|
|
|
|
{
|
|
|
|
|
Action: "accept",
|
|
|
|
|
Sources: []policyv2.Alias{policyv2.Wildcard},
|
|
|
|
|
Destinations: []policyv2.AliasWithPorts{{Alias: policyv2.Wildcard, Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}}},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"user1", "user2"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
|
|
|
|
|
|
|
|
|
err = scenario.CreateHeadscaleEnv(
|
|
|
|
|
[]tsic.Option{},
|
|
|
|
|
hsic.WithACLPolicy(policy),
|
|
|
|
|
hsic.WithTestName("tagged-reg"),
|
|
|
|
|
)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Get users (they were already created by ScenarioSpec)
|
|
|
|
|
users, err := headscale.ListUsers()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, users, 2, "Should have 2 users")
|
|
|
|
|
|
|
|
|
|
var user1, user2 *v1.User
|
|
|
|
|
|
|
|
|
|
for _, u := range users {
|
|
|
|
|
if u.GetName() == "user1" {
|
|
|
|
|
user1 = u
|
|
|
|
|
} else if u.GetName() == "user2" {
|
|
|
|
|
user2 = u
|
|
|
|
|
}
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
require.NotNil(t, user1, "Should find user1")
|
|
|
|
|
require.NotNil(t, user2, "Should find user2")
|
|
|
|
|
|
|
|
|
|
// Test 1: Create a PreAuthKey with tags
|
|
|
|
|
var taggedKey v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
2025-12-02 12:01:25 +01:00
|
|
|
"preauthkeys",
|
|
|
|
|
"--user", strconv.FormatUint(user1.GetId(), 10),
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable",
|
|
|
|
|
"--tags", "tag:server,tag:prod",
|
2025-10-23 17:57:41 +02:00
|
|
|
"--output", "json",
|
|
|
|
|
},
|
2025-12-02 12:01:25 +01:00
|
|
|
&taggedKey,
|
2025-10-23 17:57:41 +02:00
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
2025-12-02 12:01:25 +01:00
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Creating tagged PreAuthKey")
|
|
|
|
|
|
|
|
|
|
// Verify PreAuthKey has both tags
|
|
|
|
|
assert.Contains(t, taggedKey.GetAclTags(), "tag:server", "PreAuthKey should have tag:server")
|
|
|
|
|
assert.Contains(t, taggedKey.GetAclTags(), "tag:prod", "PreAuthKey should have tag:prod")
|
|
|
|
|
assert.Len(t, taggedKey.GetAclTags(), 2, "PreAuthKey should have exactly 2 tags")
|
|
|
|
|
|
|
|
|
|
// Test 2: Register a node using the tagged PreAuthKey
|
|
|
|
|
err = scenario.CreateTailscaleNodesInUser("user1", "unstable", 1, tsic.WithNetwork(scenario.Networks()[0]))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
err = scenario.RunTailscaleUp("user1", headscale.GetEndpoint(), taggedKey.GetKey())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Wait for the node to be registered
|
|
|
|
|
var registeredNode *v1.Node
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
nodes, err := headscale.ListNodes()
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
assert.GreaterOrEqual(c, len(nodes), 1, "Should have at least 1 node")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// Find the tagged node - it will have user "tagged-devices" per tags-as-identity model
|
|
|
|
|
for _, node := range nodes {
|
|
|
|
|
if node.GetUser().GetName() == "tagged-devices" && len(node.GetValidTags()) > 0 {
|
|
|
|
|
registeredNode = node
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
assert.NotNil(c, registeredNode, "Should find a tagged node")
|
|
|
|
|
}, 30*time.Second, 500*time.Millisecond, "Waiting for tagged node registration")
|
|
|
|
|
|
|
|
|
|
// Test 3: Verify the registered node has the tags from the PreAuthKey
|
|
|
|
|
assert.Contains(t, registeredNode.GetValidTags(), "tag:server", "Node should have tag:server")
|
|
|
|
|
assert.Contains(t, registeredNode.GetValidTags(), "tag:prod", "Node should have tag:prod")
|
|
|
|
|
assert.Len(t, registeredNode.GetValidTags(), 2, "Node should have exactly 2 tags")
|
|
|
|
|
|
|
|
|
|
// Test 4: Verify the node shows as TaggedDevices user (tags-as-identity model)
|
|
|
|
|
// Tagged nodes always show as "tagged-devices" in API responses, even though
|
|
|
|
|
// internally UserID may be set for "created by" tracking
|
|
|
|
|
assert.Equal(t, "tagged-devices", registeredNode.GetUser().GetName(), "Tagged node should show as tagged-devices user")
|
|
|
|
|
|
|
|
|
|
// Test 5: Verify the node is identified as tagged
|
|
|
|
|
assert.NotEmpty(t, registeredNode.GetValidTags(), "Tagged node should have tags")
|
|
|
|
|
|
|
|
|
|
// Test 6: Verify tag modification on tagged nodes
|
|
|
|
|
// NOTE: Changing tags requires complex ACL authorization where the node's IP
|
|
|
|
|
// must be authorized for the new tags via tagOwners. For simplicity, we skip
|
|
|
|
|
// this test and instead verify that tags cannot be arbitrarily changed without
|
|
|
|
|
// proper ACL authorization.
|
|
|
|
|
//
|
|
|
|
|
// This is expected behavior - tag changes must be authorized by ACL policy.
|
2024-09-07 09:23:58 +02:00
|
|
|
_, err = headscale.Execute(
|
2023-05-10 12:10:33 +02:00
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"tag",
|
2025-12-02 12:01:25 +01:00
|
|
|
"-i", strconv.FormatUint(registeredNode.GetId(), 10),
|
|
|
|
|
"-t", "tag:unauthorized",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output", "json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-12-02 12:01:25 +01:00
|
|
|
// This SHOULD fail because tag:unauthorized is not in our ACL policy
|
|
|
|
|
require.ErrorContains(t, err, "invalid or unauthorized tags", "Unauthorized tag should be rejected")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-12-02 12:01:25 +01:00
|
|
|
// Test 7: Create a user-owned node for comparison
|
|
|
|
|
var userOwnedKey v1.PreAuthKey
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
2025-12-02 12:01:25 +01:00
|
|
|
"preauthkeys",
|
|
|
|
|
"--user", strconv.FormatUint(user2.GetId(), 10),
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable",
|
2025-10-23 17:57:41 +02:00
|
|
|
"--output", "json",
|
|
|
|
|
},
|
2025-12-02 12:01:25 +01:00
|
|
|
&userOwnedKey,
|
2025-10-23 17:57:41 +02:00
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
2025-12-02 12:01:25 +01:00
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Creating user-owned PreAuthKey")
|
|
|
|
|
|
|
|
|
|
// Verify this PreAuthKey has NO tags
|
|
|
|
|
assert.Empty(t, userOwnedKey.GetAclTags(), "User-owned PreAuthKey should have no tags")
|
|
|
|
|
|
|
|
|
|
err = scenario.CreateTailscaleNodesInUser("user2", "unstable", 1, tsic.WithNetwork(scenario.Networks()[0]))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
err = scenario.RunTailscaleUp("user2", headscale.GetEndpoint(), userOwnedKey.GetKey())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Wait for the user-owned node to be registered
|
|
|
|
|
var userOwnedNode *v1.Node
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
nodes, err := headscale.ListNodes()
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
assert.GreaterOrEqual(c, len(nodes), 2, "Should have at least 2 nodes")
|
|
|
|
|
|
|
|
|
|
// Find the node registered with user2
|
|
|
|
|
for _, node := range nodes {
|
|
|
|
|
if node.GetUser().GetName() == "user2" {
|
|
|
|
|
userOwnedNode = node
|
|
|
|
|
break
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-02 12:01:25 +01:00
|
|
|
|
|
|
|
|
assert.NotNil(c, userOwnedNode, "Should find a node for user2")
|
|
|
|
|
}, 30*time.Second, 500*time.Millisecond, "Waiting for user-owned node registration")
|
|
|
|
|
|
|
|
|
|
// Test 8: Verify user-owned node has NO tags
|
|
|
|
|
assert.Empty(t, userOwnedNode.GetValidTags(), "User-owned node should have no tags")
|
|
|
|
|
assert.NotZero(t, userOwnedNode.GetUser().GetId(), "User-owned node should have UserID")
|
|
|
|
|
|
|
|
|
|
// Test 9: Verify attempting to set UNAUTHORIZED tags on user-owned node fails
|
|
|
|
|
// Note: Under tags-as-identity model, user-owned nodes CAN be converted to tagged nodes
|
|
|
|
|
// if the tags are authorized. We use an unauthorized tag to test rejection.
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"tag",
|
|
|
|
|
"-i", strconv.FormatUint(userOwnedNode.GetId(), 10),
|
|
|
|
|
"-t", "tag:not-in-policy",
|
|
|
|
|
"--output", "json",
|
|
|
|
|
},
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
2025-12-02 12:01:25 +01:00
|
|
|
require.ErrorContains(t, err, "invalid or unauthorized tags", "Setting unauthorized tags should fail")
|
|
|
|
|
|
|
|
|
|
// Test 10: Verify basic connectivity - wait for sync
|
|
|
|
|
err = scenario.WaitForTailscaleSync()
|
|
|
|
|
require.NoError(t, err, "Clients should be able to sync")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestTagPersistenceAcrossRestart validates that tags persist across container
|
|
|
|
|
// restarts and that re-authentication doesn't re-apply tags from PreAuthKey.
|
|
|
|
|
// This is a regression test for issue #2830.
|
|
|
|
|
func TestTagPersistenceAcrossRestart(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
|
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"user1"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
|
|
|
|
|
|
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("tag-persist"))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Get user
|
|
|
|
|
users, err := headscale.ListUsers()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, users, 1)
|
|
|
|
|
user1 := users[0]
|
|
|
|
|
|
|
|
|
|
// Create a reusable PreAuthKey with tags
|
|
|
|
|
var taggedKey v1.PreAuthKey
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"preauthkeys",
|
|
|
|
|
"--user", strconv.FormatUint(user1.GetId(), 10),
|
|
|
|
|
"create",
|
|
|
|
|
"--reusable", // Critical: key must be reusable for container restart
|
|
|
|
|
"--tags", "tag:server,tag:prod",
|
|
|
|
|
"--output", "json",
|
|
|
|
|
},
|
|
|
|
|
&taggedKey,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Creating reusable tagged PreAuthKey")
|
|
|
|
|
|
|
|
|
|
require.True(t, taggedKey.GetReusable(), "PreAuthKey must be reusable for restart scenario")
|
|
|
|
|
require.Contains(t, taggedKey.GetAclTags(), "tag:server")
|
|
|
|
|
require.Contains(t, taggedKey.GetAclTags(), "tag:prod")
|
|
|
|
|
|
|
|
|
|
// Register initial node with tagged PreAuthKey
|
|
|
|
|
err = scenario.CreateTailscaleNodesInUser("user1", "unstable", 1, tsic.WithNetwork(scenario.Networks()[0]))
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
err = scenario.RunTailscaleUp("user1", headscale.GetEndpoint(), taggedKey.GetKey())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Wait for node registration and get initial node state
|
|
|
|
|
var initialNode *v1.Node
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
nodes, err := headscale.ListNodes()
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
assert.GreaterOrEqual(c, len(nodes), 1, "Should have at least 1 node")
|
|
|
|
|
|
|
|
|
|
for _, node := range nodes {
|
|
|
|
|
if node.GetUser().GetId() == user1.GetId() || node.GetUser().GetName() == "tagged-devices" {
|
|
|
|
|
initialNode = node
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.NotNil(c, initialNode, "Should find the registered node")
|
|
|
|
|
}, 30*time.Second, 500*time.Millisecond, "Waiting for initial node registration")
|
|
|
|
|
|
|
|
|
|
// Verify initial tags
|
|
|
|
|
require.Contains(t, initialNode.GetValidTags(), "tag:server", "Initial node should have tag:server")
|
|
|
|
|
require.Contains(t, initialNode.GetValidTags(), "tag:prod", "Initial node should have tag:prod")
|
|
|
|
|
require.Len(t, initialNode.GetValidTags(), 2, "Initial node should have exactly 2 tags")
|
|
|
|
|
|
|
|
|
|
initialNodeID := initialNode.GetId()
|
|
|
|
|
t.Logf("Initial node registered with ID %d and tags %v", initialNodeID, initialNode.GetValidTags())
|
|
|
|
|
|
|
|
|
|
// Simulate container restart by shutting down and restarting Tailscale client
|
|
|
|
|
allClients, err := scenario.ListTailscaleClients()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, allClients, 1, "Should have exactly 1 client")
|
|
|
|
|
|
|
|
|
|
client := allClients[0]
|
|
|
|
|
|
|
|
|
|
// Stop the client (simulates container stop)
|
|
|
|
|
err = client.Down()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Wait a bit to ensure the client is fully stopped
|
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
|
|
|
|
|
|
// Restart the client with the SAME PreAuthKey (container restart scenario)
|
|
|
|
|
// This simulates what happens when a Docker container restarts with a reusable PreAuthKey
|
|
|
|
|
err = scenario.RunTailscaleUp("user1", headscale.GetEndpoint(), taggedKey.GetKey())
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Wait for re-authentication
|
|
|
|
|
var nodeAfterRestart *v1.Node
|
|
|
|
|
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
nodes, err := headscale.ListNodes()
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
|
|
|
|
|
for _, node := range nodes {
|
|
|
|
|
if node.GetId() == initialNodeID {
|
|
|
|
|
nodeAfterRestart = node
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.NotNil(c, nodeAfterRestart, "Should find the same node after restart")
|
|
|
|
|
}, 30*time.Second, 500*time.Millisecond, "Waiting for node re-authentication")
|
|
|
|
|
|
|
|
|
|
// CRITICAL ASSERTION: Tags should NOT be re-applied from PreAuthKey
|
|
|
|
|
// Tags are only applied during INITIAL authentication, not re-authentication
|
|
|
|
|
// The node should keep its existing tags (which happen to be the same in this case)
|
|
|
|
|
assert.Contains(t, nodeAfterRestart.GetValidTags(), "tag:server", "Node should still have tag:server after restart")
|
|
|
|
|
assert.Contains(t, nodeAfterRestart.GetValidTags(), "tag:prod", "Node should still have tag:prod after restart")
|
|
|
|
|
assert.Len(t, nodeAfterRestart.GetValidTags(), 2, "Node should still have exactly 2 tags after restart")
|
|
|
|
|
|
|
|
|
|
// Verify it's the SAME node (same ID), not a new registration
|
|
|
|
|
assert.Equal(t, initialNodeID, nodeAfterRestart.GetId(), "Should be the same node, not a new registration")
|
|
|
|
|
|
|
|
|
|
// Verify node count hasn't increased (no duplicate nodes)
|
|
|
|
|
finalNodes, err := headscale.ListNodes()
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Len(t, finalNodes, 1, "Should still have exactly 1 node (no duplicates from restart)")
|
|
|
|
|
|
|
|
|
|
t.Logf("Container restart validation complete - node %d maintained tags across restart", initialNodeID)
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-26 15:16:06 +01:00
|
|
|
func TestNodeAdvertiseTagCommand(t *testing.T) {
|
2024-01-04 21:26:49 +01:00
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2024-11-26 15:16:06 +01:00
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
2025-05-20 13:57:26 +02:00
|
|
|
policy *policyv2.Policy
|
2024-11-26 15:16:06 +01:00
|
|
|
wantTag bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "no-policy",
|
|
|
|
|
wantTag: false,
|
2024-01-04 21:26:49 +01:00
|
|
|
},
|
2024-11-26 15:16:06 +01:00
|
|
|
{
|
2024-12-19 13:10:10 +01:00
|
|
|
name: "with-policy-email",
|
2025-05-20 13:57:26 +02:00
|
|
|
policy: &policyv2.Policy{
|
|
|
|
|
ACLs: []policyv2.ACL{
|
2024-12-19 13:10:10 +01:00
|
|
|
{
|
2025-05-20 13:57:26 +02:00
|
|
|
Action: "accept",
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
Sources: []policyv2.Alias{wildcard()},
|
|
|
|
|
Destinations: []policyv2.AliasWithPorts{
|
|
|
|
|
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
|
|
|
|
},
|
2024-12-19 13:10:10 +01:00
|
|
|
},
|
|
|
|
|
},
|
2025-05-20 13:57:26 +02:00
|
|
|
TagOwners: policyv2.TagOwners{
|
|
|
|
|
policyv2.Tag("tag:test"): policyv2.Owners{usernameOwner("user1@test.no")},
|
2024-12-19 13:10:10 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
wantTag: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "with-policy-username",
|
2025-05-20 13:57:26 +02:00
|
|
|
policy: &policyv2.Policy{
|
|
|
|
|
ACLs: []policyv2.ACL{
|
2024-11-22 16:54:58 +01:00
|
|
|
{
|
2025-05-20 13:57:26 +02:00
|
|
|
Action: "accept",
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
Sources: []policyv2.Alias{wildcard()},
|
|
|
|
|
Destinations: []policyv2.AliasWithPorts{
|
|
|
|
|
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
|
|
|
|
},
|
2024-11-22 16:54:58 +01:00
|
|
|
},
|
|
|
|
|
},
|
2025-05-20 13:57:26 +02:00
|
|
|
TagOwners: policyv2.TagOwners{
|
|
|
|
|
policyv2.Tag("tag:test"): policyv2.Owners{usernameOwner("user1@")},
|
2024-01-04 21:26:49 +01:00
|
|
|
},
|
|
|
|
|
},
|
2024-11-26 15:16:06 +01:00
|
|
|
wantTag: true,
|
|
|
|
|
},
|
2024-12-19 13:10:10 +01:00
|
|
|
{
|
|
|
|
|
name: "with-policy-groups",
|
2025-05-20 13:57:26 +02:00
|
|
|
policy: &policyv2.Policy{
|
|
|
|
|
Groups: policyv2.Groups{
|
|
|
|
|
policyv2.Group("group:admins"): []policyv2.Username{policyv2.Username("user1@")},
|
2024-12-19 13:10:10 +01:00
|
|
|
},
|
2025-05-20 13:57:26 +02:00
|
|
|
ACLs: []policyv2.ACL{
|
2024-12-19 13:10:10 +01:00
|
|
|
{
|
2025-05-20 13:57:26 +02:00
|
|
|
Action: "accept",
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
Sources: []policyv2.Alias{wildcard()},
|
|
|
|
|
Destinations: []policyv2.AliasWithPorts{
|
|
|
|
|
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
|
|
|
|
},
|
2024-12-19 13:10:10 +01:00
|
|
|
},
|
|
|
|
|
},
|
2025-05-20 13:57:26 +02:00
|
|
|
TagOwners: policyv2.TagOwners{
|
|
|
|
|
policyv2.Tag("tag:test"): policyv2.Owners{groupOwner("group:admins")},
|
2024-12-19 13:10:10 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
wantTag: true,
|
|
|
|
|
},
|
2024-11-26 15:16:06 +01:00
|
|
|
}
|
2024-01-04 21:26:49 +01:00
|
|
|
|
2024-11-26 15:16:06 +01:00
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
NodesPerUser: 1,
|
|
|
|
|
Users: []string{"user1"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-12-19 13:10:10 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2024-01-04 21:26:49 +01:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv(
|
2024-11-26 15:16:06 +01:00
|
|
|
[]tsic.Option{tsic.WithTags([]string{"tag:test"})},
|
|
|
|
|
hsic.WithTestName("cliadvtags"),
|
|
|
|
|
hsic.WithACLPolicy(tt.policy),
|
|
|
|
|
)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-11-26 15:16:06 +01:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-11-26 15:16:06 +01:00
|
|
|
|
|
|
|
|
// Test list all nodes after added seconds
|
2025-10-23 17:57:41 +02:00
|
|
|
var resultMachines []*v1.Node
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
resultMachines = make([]*v1.Node, spec.NodesPerUser)
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--tags",
|
|
|
|
|
"--output", "json",
|
|
|
|
|
},
|
|
|
|
|
&resultMachines,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
found := false
|
|
|
|
|
for _, node := range resultMachines {
|
|
|
|
|
if tags := node.GetValidTags(); tags != nil {
|
|
|
|
|
found = slices.Contains(tags, "tag:test")
|
|
|
|
|
}
|
2024-01-04 21:26:49 +01:00
|
|
|
}
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.Equalf(
|
|
|
|
|
c,
|
|
|
|
|
tt.wantTag,
|
|
|
|
|
found,
|
|
|
|
|
"'tag:test' found(%t) is the list of nodes, expected %t", found, tt.wantTag,
|
|
|
|
|
)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for tag propagation to nodes")
|
2024-11-26 15:16:06 +01:00
|
|
|
})
|
2024-01-04 21:26:49 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-10 12:10:33 +02:00
|
|
|
func TestNodeCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"node-user", "other-user"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clins"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
regIDs := []string{
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
2025-01-26 22:20:11 +01:00
|
|
|
nodes := make([]*v1.Node, len(regIDs))
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
for index, regID := range regIDs {
|
2023-05-10 12:10:33 +02:00
|
|
|
_, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"debug",
|
|
|
|
|
"create-node",
|
|
|
|
|
"--name",
|
2023-09-24 13:42:05 +02:00
|
|
|
fmt.Sprintf("node-%d", index+1),
|
2023-05-10 12:10:33 +02:00
|
|
|
"--user",
|
2023-09-24 13:42:05 +02:00
|
|
|
"node-user",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--key",
|
2025-01-26 22:20:11 +01:00
|
|
|
regID,
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
var node v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"--user",
|
|
|
|
|
"node-user",
|
|
|
|
|
"register",
|
|
|
|
|
"--key",
|
|
|
|
|
regID,
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&node,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for node registration")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
nodes[index] = &node
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
assert.Len(ct, nodes, len(regIDs), "Should have correct number of nodes after CLI operations")
|
|
|
|
|
}, 15*time.Second, 1*time.Second)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
// Test list all nodes after added seconds
|
2023-09-24 13:42:05 +02:00
|
|
|
var listAll []v1.Node
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
err := executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAll,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(ct, err)
|
|
|
|
|
assert.Len(ct, listAll, len(regIDs), "Should list all nodes after CLI operations")
|
|
|
|
|
}, 20*time.Second, 1*time.Second)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.Equal(t, uint64(1), listAll[0].GetId())
|
|
|
|
|
assert.Equal(t, uint64(2), listAll[1].GetId())
|
|
|
|
|
assert.Equal(t, uint64(3), listAll[2].GetId())
|
|
|
|
|
assert.Equal(t, uint64(4), listAll[3].GetId())
|
|
|
|
|
assert.Equal(t, uint64(5), listAll[4].GetId())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.Equal(t, "node-1", listAll[0].GetName())
|
|
|
|
|
assert.Equal(t, "node-2", listAll[1].GetName())
|
|
|
|
|
assert.Equal(t, "node-3", listAll[2].GetName())
|
|
|
|
|
assert.Equal(t, "node-4", listAll[3].GetName())
|
|
|
|
|
assert.Equal(t, "node-5", listAll[4].GetName())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
otherUserRegIDs := []string{
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
2025-01-26 22:20:11 +01:00
|
|
|
otherUserMachines := make([]*v1.Node, len(otherUserRegIDs))
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
for index, regID := range otherUserRegIDs {
|
2023-05-10 12:10:33 +02:00
|
|
|
_, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"debug",
|
|
|
|
|
"create-node",
|
|
|
|
|
"--name",
|
2025-10-22 13:50:39 +02:00
|
|
|
fmt.Sprintf("otheruser-node-%d", index+1),
|
2023-05-10 12:10:33 +02:00
|
|
|
"--user",
|
|
|
|
|
"other-user",
|
|
|
|
|
"--key",
|
2025-01-26 22:20:11 +01:00
|
|
|
regID,
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
var node v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"--user",
|
|
|
|
|
"other-user",
|
|
|
|
|
"register",
|
|
|
|
|
"--key",
|
|
|
|
|
regID,
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&node,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for other-user node registration")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
otherUserMachines[index] = &node
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
assert.Len(ct, otherUserMachines, len(otherUserRegIDs), "Should have correct number of otherUser machines after CLI operations")
|
|
|
|
|
}, 15*time.Second, 1*time.Second)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
// Test list all nodes after added otherUser
|
2023-09-24 13:42:05 +02:00
|
|
|
var listAllWithotherUser []v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAllWithotherUser,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for nodes list after adding other-user nodes")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
// All nodes, nodes + otherUser
|
2023-05-10 12:10:33 +02:00
|
|
|
assert.Len(t, listAllWithotherUser, 7)
|
|
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.Equal(t, uint64(6), listAllWithotherUser[5].GetId())
|
|
|
|
|
assert.Equal(t, uint64(7), listAllWithotherUser[6].GetId())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-10-22 13:50:39 +02:00
|
|
|
assert.Equal(t, "otheruser-node-1", listAllWithotherUser[5].GetName())
|
|
|
|
|
assert.Equal(t, "otheruser-node-2", listAllWithotherUser[6].GetName())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
// Test list all nodes after added otherUser
|
2023-09-24 13:42:05 +02:00
|
|
|
var listOnlyotherUserMachineUser []v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--user",
|
|
|
|
|
"other-user",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listOnlyotherUserMachineUser,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for nodes list filtered by other-user")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Len(t, listOnlyotherUserMachineUser, 2)
|
|
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.Equal(t, uint64(6), listOnlyotherUserMachineUser[0].GetId())
|
|
|
|
|
assert.Equal(t, uint64(7), listOnlyotherUserMachineUser[1].GetId())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Equal(
|
|
|
|
|
t,
|
2025-10-22 13:50:39 +02:00
|
|
|
"otheruser-node-1",
|
2023-11-23 08:31:33 +01:00
|
|
|
listOnlyotherUserMachineUser[0].GetName(),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
assert.Equal(
|
|
|
|
|
t,
|
2025-10-22 13:50:39 +02:00
|
|
|
"otheruser-node-2",
|
2023-11-23 08:31:33 +01:00
|
|
|
listOnlyotherUserMachineUser[1].GetName(),
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
// Delete a nodes
|
2023-05-10 12:10:33 +02:00
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"delete",
|
|
|
|
|
"--identifier",
|
|
|
|
|
// Delete the last added machine
|
|
|
|
|
"4",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
"--force",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
// Test: list main user after node is deleted
|
|
|
|
|
var listOnlyMachineUserAfterDelete []v1.Node
|
2025-07-13 17:37:11 +02:00
|
|
|
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
|
|
|
|
|
err := executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--user",
|
|
|
|
|
"node-user",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listOnlyMachineUserAfterDelete,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(ct, err)
|
|
|
|
|
assert.Len(ct, listOnlyMachineUserAfterDelete, 4, "Should have 4 nodes for node-user after deletion")
|
|
|
|
|
}, 20*time.Second, 1*time.Second)
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNodeExpireCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"node-expire-user"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clins"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
regIDs := []string{
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
2025-01-26 22:20:11 +01:00
|
|
|
nodes := make([]*v1.Node, len(regIDs))
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
for index, regID := range regIDs {
|
2023-05-10 12:10:33 +02:00
|
|
|
_, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"debug",
|
|
|
|
|
"create-node",
|
|
|
|
|
"--name",
|
2023-09-24 13:42:05 +02:00
|
|
|
fmt.Sprintf("node-%d", index+1),
|
2023-05-10 12:10:33 +02:00
|
|
|
"--user",
|
2023-09-24 13:42:05 +02:00
|
|
|
"node-expire-user",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--key",
|
2025-01-26 22:20:11 +01:00
|
|
|
regID,
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
var node v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"--user",
|
|
|
|
|
"node-expire-user",
|
|
|
|
|
"register",
|
|
|
|
|
"--key",
|
|
|
|
|
regID,
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&node,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for node-expire-user node registration")
|
|
|
|
|
|
|
|
|
|
nodes[index] = &node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.Len(t, nodes, len(regIDs))
|
|
|
|
|
|
|
|
|
|
var listAll []v1.Node
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
2023-05-10 12:10:33 +02:00
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
2025-10-23 17:57:41 +02:00
|
|
|
"list",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
2025-10-23 17:57:41 +02:00
|
|
|
&listAll,
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for nodes list in expire test")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Len(t, listAll, 5)
|
|
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.True(t, listAll[0].GetExpiry().AsTime().IsZero())
|
|
|
|
|
assert.True(t, listAll[1].GetExpiry().AsTime().IsZero())
|
|
|
|
|
assert.True(t, listAll[2].GetExpiry().AsTime().IsZero())
|
|
|
|
|
assert.True(t, listAll[3].GetExpiry().AsTime().IsZero())
|
|
|
|
|
assert.True(t, listAll[4].GetExpiry().AsTime().IsZero())
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-03-30 13:19:05 +02:00
|
|
|
for idx := range 3 {
|
2023-05-10 12:10:33 +02:00
|
|
|
_, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"expire",
|
|
|
|
|
"--identifier",
|
2025-07-10 23:38:55 +02:00
|
|
|
strconv.FormatUint(listAll[idx].GetId(), 10),
|
2023-05-10 12:10:33 +02:00
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
var listAllAfterExpiry []v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAllAfterExpiry,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for nodes list after expiry")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Len(t, listAllAfterExpiry, 5)
|
|
|
|
|
|
2023-11-23 08:31:33 +01:00
|
|
|
assert.True(t, listAllAfterExpiry[0].GetExpiry().AsTime().Before(time.Now()))
|
|
|
|
|
assert.True(t, listAllAfterExpiry[1].GetExpiry().AsTime().Before(time.Now()))
|
|
|
|
|
assert.True(t, listAllAfterExpiry[2].GetExpiry().AsTime().Before(time.Now()))
|
|
|
|
|
assert.True(t, listAllAfterExpiry[3].GetExpiry().AsTime().IsZero())
|
|
|
|
|
assert.True(t, listAllAfterExpiry[4].GetExpiry().AsTime().IsZero())
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNodeRenameCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"node-rename-command"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
err = scenario.CreateHeadscaleEnv([]tsic.Option{}, hsic.WithTestName("clins"))
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
regIDs := []string{
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
|
|
|
|
types.MustRegistrationID().String(),
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
2025-01-26 22:20:11 +01:00
|
|
|
nodes := make([]*v1.Node, len(regIDs))
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-01-26 22:20:11 +01:00
|
|
|
for index, regID := range regIDs {
|
2023-05-10 12:10:33 +02:00
|
|
|
_, err := headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"debug",
|
|
|
|
|
"create-node",
|
|
|
|
|
"--name",
|
2023-09-24 13:42:05 +02:00
|
|
|
fmt.Sprintf("node-%d", index+1),
|
2023-05-10 12:10:33 +02:00
|
|
|
"--user",
|
2023-09-24 13:42:05 +02:00
|
|
|
"node-rename-command",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--key",
|
2025-01-26 22:20:11 +01:00
|
|
|
regID,
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
var node v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"--user",
|
|
|
|
|
"node-rename-command",
|
|
|
|
|
"register",
|
|
|
|
|
"--key",
|
|
|
|
|
regID,
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&node,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for node-rename-command node registration")
|
|
|
|
|
|
|
|
|
|
nodes[index] = &node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert.Len(t, nodes, len(regIDs))
|
|
|
|
|
|
|
|
|
|
var listAll []v1.Node
|
|
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
2023-05-10 12:10:33 +02:00
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
2025-10-23 17:57:41 +02:00
|
|
|
"list",
|
2023-05-10 12:10:33 +02:00
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
2025-10-23 17:57:41 +02:00
|
|
|
&listAll,
|
2023-05-10 12:10:33 +02:00
|
|
|
)
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for nodes list in rename test")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Len(t, listAll, 5)
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
assert.Contains(t, listAll[0].GetGivenName(), "node-1")
|
|
|
|
|
assert.Contains(t, listAll[1].GetGivenName(), "node-2")
|
|
|
|
|
assert.Contains(t, listAll[2].GetGivenName(), "node-3")
|
|
|
|
|
assert.Contains(t, listAll[3].GetGivenName(), "node-4")
|
|
|
|
|
assert.Contains(t, listAll[4].GetGivenName(), "node-5")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2025-03-30 13:19:05 +02:00
|
|
|
for idx := range 3 {
|
2024-02-08 17:28:19 +01:00
|
|
|
res, err := headscale.Execute(
|
2023-05-10 12:10:33 +02:00
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"rename",
|
|
|
|
|
"--identifier",
|
2025-07-10 23:38:55 +02:00
|
|
|
strconv.FormatUint(listAll[idx].GetId(), 10),
|
2023-09-24 13:42:05 +02:00
|
|
|
fmt.Sprintf("newnode-%d", idx+1),
|
2023-05-10 12:10:33 +02:00
|
|
|
},
|
|
|
|
|
)
|
2025-07-10 23:38:55 +02:00
|
|
|
assert.NoError(t, err)
|
2024-02-08 17:28:19 +01:00
|
|
|
|
|
|
|
|
assert.Contains(t, res, "Node renamed")
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
var listAllAfterRename []v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAllAfterRename,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for nodes list after rename")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Len(t, listAllAfterRename, 5)
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
assert.Equal(t, "newnode-1", listAllAfterRename[0].GetGivenName())
|
|
|
|
|
assert.Equal(t, "newnode-2", listAllAfterRename[1].GetGivenName())
|
|
|
|
|
assert.Equal(t, "newnode-3", listAllAfterRename[2].GetGivenName())
|
|
|
|
|
assert.Contains(t, listAllAfterRename[3].GetGivenName(), "node-4")
|
|
|
|
|
assert.Contains(t, listAllAfterRename[4].GetGivenName(), "node-5")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
// Test failure for too long names
|
2024-09-07 09:23:58 +02:00
|
|
|
_, err = headscale.Execute(
|
2023-05-10 12:10:33 +02:00
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"rename",
|
|
|
|
|
"--identifier",
|
2025-07-10 23:38:55 +02:00
|
|
|
strconv.FormatUint(listAll[4].GetId(), 10),
|
2024-09-07 09:23:58 +02:00
|
|
|
strings.Repeat("t", 64),
|
2023-05-10 12:10:33 +02:00
|
|
|
},
|
|
|
|
|
)
|
2025-10-22 13:50:39 +02:00
|
|
|
assert.ErrorContains(t, err, "must not exceed 63 characters")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
var listAllAfterRenameAttempt []v1.Node
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"nodes",
|
|
|
|
|
"list",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&listAllAfterRenameAttempt,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for nodes list after failed rename attempt")
|
2023-05-10 12:10:33 +02:00
|
|
|
|
|
|
|
|
assert.Len(t, listAllAfterRenameAttempt, 5)
|
|
|
|
|
|
2023-09-24 13:42:05 +02:00
|
|
|
assert.Equal(t, "newnode-1", listAllAfterRenameAttempt[0].GetGivenName())
|
|
|
|
|
assert.Equal(t, "newnode-2", listAllAfterRenameAttempt[1].GetGivenName())
|
|
|
|
|
assert.Equal(t, "newnode-3", listAllAfterRenameAttempt[2].GetGivenName())
|
|
|
|
|
assert.Contains(t, listAllAfterRenameAttempt[3].GetGivenName(), "node-4")
|
|
|
|
|
assert.Contains(t, listAllAfterRenameAttempt[4].GetGivenName(), "node-5")
|
2023-05-10 12:10:33 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-18 11:08:25 +05:30
|
|
|
func TestPolicyCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
Users: []string{"user1"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2024-07-18 11:08:25 +05:30
|
|
|
|
|
|
|
|
err = scenario.CreateHeadscaleEnv(
|
|
|
|
|
[]tsic.Option{},
|
|
|
|
|
hsic.WithTestName("clins"),
|
|
|
|
|
hsic.WithConfigEnv(map[string]string{
|
|
|
|
|
"HEADSCALE_POLICY_MODE": "database",
|
|
|
|
|
}),
|
|
|
|
|
)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-07-18 11:08:25 +05:30
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-07-18 11:08:25 +05:30
|
|
|
|
2025-05-20 13:57:26 +02:00
|
|
|
p := policyv2.Policy{
|
|
|
|
|
ACLs: []policyv2.ACL{
|
2024-07-18 11:08:25 +05:30
|
|
|
{
|
2025-05-20 13:57:26 +02:00
|
|
|
Action: "accept",
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
Sources: []policyv2.Alias{wildcard()},
|
|
|
|
|
Destinations: []policyv2.AliasWithPorts{
|
|
|
|
|
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
|
|
|
|
},
|
2024-07-18 11:08:25 +05:30
|
|
|
},
|
|
|
|
|
},
|
2025-05-20 13:57:26 +02:00
|
|
|
TagOwners: policyv2.TagOwners{
|
|
|
|
|
policyv2.Tag("tag:exists"): policyv2.Owners{usernameOwner("user1@")},
|
2024-07-18 11:08:25 +05:30
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pBytes, _ := json.Marshal(p)
|
|
|
|
|
|
|
|
|
|
policyFilePath := "/etc/headscale/policy.json"
|
|
|
|
|
|
|
|
|
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-07-18 11:08:25 +05:30
|
|
|
|
|
|
|
|
// No policy is present at this time.
|
|
|
|
|
// Add a new policy from a file.
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"policy",
|
|
|
|
|
"set",
|
|
|
|
|
"-f",
|
|
|
|
|
policyFilePath,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-07-18 11:08:25 +05:30
|
|
|
|
|
|
|
|
// Get the current policy and check
|
|
|
|
|
// if it is the same as the one we set.
|
2025-05-20 13:57:26 +02:00
|
|
|
var output *policyv2.Policy
|
2025-10-23 17:57:41 +02:00
|
|
|
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
|
|
|
|
err = executeAndUnmarshal(
|
|
|
|
|
headscale,
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"policy",
|
|
|
|
|
"get",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
&output,
|
|
|
|
|
)
|
|
|
|
|
assert.NoError(c, err)
|
|
|
|
|
}, 10*time.Second, 200*time.Millisecond, "Waiting for policy get command")
|
2024-07-18 11:08:25 +05:30
|
|
|
|
|
|
|
|
assert.Len(t, output.TagOwners, 1)
|
|
|
|
|
assert.Len(t, output.ACLs, 1)
|
|
|
|
|
}
|
2024-08-30 16:58:29 +02:00
|
|
|
|
|
|
|
|
func TestPolicyBrokenConfigCommand(t *testing.T) {
|
|
|
|
|
IntegrationSkip(t)
|
|
|
|
|
|
2025-03-21 11:49:32 +01:00
|
|
|
spec := ScenarioSpec{
|
|
|
|
|
NodesPerUser: 1,
|
|
|
|
|
Users: []string{"user1"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scenario, err := NewScenario(spec)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-09-17 10:44:55 +01:00
|
|
|
defer scenario.ShutdownAssertNoPanics(t)
|
2024-08-30 16:58:29 +02:00
|
|
|
|
|
|
|
|
err = scenario.CreateHeadscaleEnv(
|
|
|
|
|
[]tsic.Option{},
|
|
|
|
|
hsic.WithTestName("clins"),
|
|
|
|
|
hsic.WithConfigEnv(map[string]string{
|
|
|
|
|
"HEADSCALE_POLICY_MODE": "database",
|
|
|
|
|
}),
|
|
|
|
|
)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-08-30 16:58:29 +02:00
|
|
|
|
|
|
|
|
headscale, err := scenario.Headscale()
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-08-30 16:58:29 +02:00
|
|
|
|
2025-05-20 13:57:26 +02:00
|
|
|
p := policyv2.Policy{
|
|
|
|
|
ACLs: []policyv2.ACL{
|
2024-08-30 16:58:29 +02:00
|
|
|
{
|
|
|
|
|
// This is an unknown action, so it will return an error
|
|
|
|
|
// and the config will not be applied.
|
2025-05-20 13:57:26 +02:00
|
|
|
Action: "unknown-action",
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
Sources: []policyv2.Alias{wildcard()},
|
|
|
|
|
Destinations: []policyv2.AliasWithPorts{
|
|
|
|
|
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
|
|
|
|
},
|
2024-08-30 16:58:29 +02:00
|
|
|
},
|
|
|
|
|
},
|
2025-05-20 13:57:26 +02:00
|
|
|
TagOwners: policyv2.TagOwners{
|
|
|
|
|
policyv2.Tag("tag:exists"): policyv2.Owners{usernameOwner("user1@")},
|
2024-08-30 16:58:29 +02:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pBytes, _ := json.Marshal(p)
|
|
|
|
|
|
|
|
|
|
policyFilePath := "/etc/headscale/policy.json"
|
|
|
|
|
|
|
|
|
|
err = headscale.WriteFile(policyFilePath, pBytes)
|
2025-10-16 12:17:43 +02:00
|
|
|
require.NoError(t, err)
|
2024-08-30 16:58:29 +02:00
|
|
|
|
|
|
|
|
// No policy is present at this time.
|
|
|
|
|
// Add a new policy from a file.
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"policy",
|
|
|
|
|
"set",
|
|
|
|
|
"-f",
|
|
|
|
|
policyFilePath,
|
|
|
|
|
},
|
|
|
|
|
)
|
2025-09-12 14:47:56 +02:00
|
|
|
assert.ErrorContains(t, err, `invalid action "unknown-action"`)
|
2024-08-30 16:58:29 +02:00
|
|
|
|
|
|
|
|
// The new policy was invalid, the old one should still be in place, which
|
|
|
|
|
// is none.
|
|
|
|
|
_, err = headscale.Execute(
|
|
|
|
|
[]string{
|
|
|
|
|
"headscale",
|
|
|
|
|
"policy",
|
|
|
|
|
"get",
|
|
|
|
|
"--output",
|
|
|
|
|
"json",
|
|
|
|
|
},
|
|
|
|
|
)
|
2024-11-26 15:16:06 +01:00
|
|
|
assert.ErrorContains(t, err, "acl policy not found")
|
2024-08-30 16:58:29 +02:00
|
|
|
}
|