mirror of
https://github.com/juanfont/headscale.git
synced 2026-03-28 12:12:06 +01:00
tags: process tags on registration, simplify policy (#2931)
This PR investigates, adds tests and aims to correctly implement Tailscale's model for how Tags should be accepted, assigned and used to identify nodes in the Tailscale access and ownership model. When evaluating in Headscale's policy, Tags are now only checked against a nodes "tags" list, which defines the source of truth for all tags for a given node. This simplifies the code for dealing with tags greatly, and should help us have less access bugs related to nodes belonging to tags or users. A node can either be owned by a user, or a tag. Next, to ensure the tags list on the node is correctly implemented, we first add tests for every registration scenario and combination of user, pre auth key and pre auth key with tags with the same registration expectation as observed by trying them all with the Tailscale control server. This should ensure that we implement the correct behaviour and that it does not change or break over time. Lastly, the missing parts of the auth has been added, or changed in the cases where it was wrong. This has in large parts allowed us to delete and simplify a lot of code. Now, tags can only be changed when a node authenticates or if set via the CLI/API. Tags can only be fully overwritten/replaced and any use of either auth or CLI will replace the current set if different. A user owned device can be converted to a tagged device, but it cannot be changed back. A tagged device can never remove the last tag either, it has to have a minimum of one.
This commit is contained in:
@@ -1383,27 +1383,31 @@ func TestACLAutogroupTagged(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create users and nodes manually with specific tags
|
||||
// Tags are now set via PreAuthKey (tags-as-identity model), not via --advertise-tags
|
||||
for _, userStr := range spec.Users {
|
||||
user, err := scenario.CreateUser(userStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a single pre-auth key per user
|
||||
authKey, err := scenario.CreatePreAuthKey(user.GetId(), true, false)
|
||||
// Create two pre-auth keys per user: one tagged, one untagged
|
||||
taggedAuthKey, err := scenario.CreatePreAuthKeyWithTags(user.GetId(), true, false, []string{"tag:test"})
|
||||
require.NoError(t, err)
|
||||
|
||||
untaggedAuthKey, err := scenario.CreatePreAuthKey(user.GetId(), true, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create nodes with proper naming
|
||||
for i := range spec.NodesPerUser {
|
||||
var tags []string
|
||||
var authKey string
|
||||
var version string
|
||||
|
||||
if i == 0 {
|
||||
// First node is tagged
|
||||
tags = []string{"tag:test"}
|
||||
// First node is tagged - use tagged PreAuthKey
|
||||
authKey = taggedAuthKey.GetKey()
|
||||
version = "head"
|
||||
t.Logf("Creating tagged node for %s", userStr)
|
||||
} else {
|
||||
// Second node is untagged
|
||||
tags = nil
|
||||
// Second node is untagged - use untagged PreAuthKey
|
||||
authKey = untaggedAuthKey.GetKey()
|
||||
version = "unstable"
|
||||
t.Logf("Creating untagged node for %s", userStr)
|
||||
}
|
||||
@@ -1429,11 +1433,6 @@ func TestACLAutogroupTagged(t *testing.T) {
|
||||
tsic.WithDockerWorkdir("/"),
|
||||
}
|
||||
|
||||
// Add tags if this is a tagged node
|
||||
if len(tags) > 0 {
|
||||
opts = append(opts, tsic.WithTags(tags))
|
||||
}
|
||||
|
||||
tsClient, err := tsic.New(
|
||||
scenario.Pool(),
|
||||
version,
|
||||
@@ -1444,8 +1443,8 @@ func TestACLAutogroupTagged(t *testing.T) {
|
||||
err = tsClient.WaitForNeedsLogin(integrationutil.PeerSyncTimeout())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Login with the auth key
|
||||
err = tsClient.Login(headscale.GetEndpoint(), authKey.GetKey())
|
||||
// Login with the appropriate auth key (tags come from the PreAuthKey)
|
||||
err = tsClient.Login(headscale.GetEndpoint(), authKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tsClient.WaitForRunning(integrationutil.PeerSyncTimeout())
|
||||
@@ -1699,17 +1698,17 @@ func TestACLAutogroupSelf(t *testing.T) {
|
||||
routerUser, err := scenario.CreateUser("user-router")
|
||||
require.NoError(t, err)
|
||||
|
||||
authKey, err := scenario.CreatePreAuthKey(routerUser.GetId(), true, false)
|
||||
// Create a tagged PreAuthKey for the router node (tags-as-identity model)
|
||||
authKey, err := scenario.CreatePreAuthKeyWithTags(routerUser.GetId(), true, false, []string{"tag:router-node"})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create router node (tagged with tag:router-node)
|
||||
// Create router node (tags come from the PreAuthKey)
|
||||
routerClient, err := tsic.New(
|
||||
scenario.Pool(),
|
||||
"unstable",
|
||||
tsic.WithCACert(headscale.GetCert()),
|
||||
tsic.WithHeadscaleName(headscale.GetHostname()),
|
||||
tsic.WithNetwork(network),
|
||||
tsic.WithTags([]string{"tag:router-node"}),
|
||||
tsic.WithNetfilter("off"),
|
||||
tsic.WithDockerEntrypoint([]string{
|
||||
"/bin/sh",
|
||||
|
||||
@@ -833,602 +833,6 @@ func TestApiKeyCommand(t *testing.T) {
|
||||
assert.Len(t, listedAPIKeysAfterDelete, 4)
|
||||
}
|
||||
|
||||
func TestNodeTagCommand(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("clins"))
|
||||
require.NoError(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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",
|
||||
},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var userOwnedNode v1.Node
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"--user",
|
||||
"user1",
|
||||
"register",
|
||||
"--key",
|
||||
regID,
|
||||
"--output",
|
||||
"json",
|
||||
},
|
||||
&userOwnedNode,
|
||||
)
|
||||
assert.NoError(c, err)
|
||||
}, 10*time.Second, 200*time.Millisecond, "Waiting for user-owned node registration")
|
||||
|
||||
// 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")
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"preauthkeys",
|
||||
"--user", strconv.FormatUint(user1.GetId(), 10),
|
||||
"create",
|
||||
"--reusable",
|
||||
"--tags", "tag:server,tag:prod",
|
||||
"--output", "json",
|
||||
},
|
||||
&taggedKey,
|
||||
)
|
||||
assert.NoError(c, err)
|
||||
}, 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")
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
_, err = headscale.Execute(
|
||||
[]string{
|
||||
"headscale",
|
||||
"nodes",
|
||||
"tag",
|
||||
"-i", strconv.FormatUint(registeredNode.GetId(), 10),
|
||||
"-t", "tag:unauthorized",
|
||||
"--output", "json",
|
||||
},
|
||||
)
|
||||
// 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")
|
||||
|
||||
// Test 7: Create a user-owned node for comparison
|
||||
var userOwnedKey v1.PreAuthKey
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
err = executeAndUnmarshal(
|
||||
headscale,
|
||||
[]string{
|
||||
"headscale",
|
||||
"preauthkeys",
|
||||
"--user", strconv.FormatUint(user2.GetId(), 10),
|
||||
"create",
|
||||
"--reusable",
|
||||
"--output", "json",
|
||||
},
|
||||
&userOwnedKey,
|
||||
)
|
||||
assert.NoError(c, err)
|
||||
}, 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
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
func TestNodeAdvertiseTagCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
policy *policyv2.Policy
|
||||
wantTag bool
|
||||
}{
|
||||
{
|
||||
name: "no-policy",
|
||||
wantTag: false,
|
||||
},
|
||||
{
|
||||
name: "with-policy-email",
|
||||
policy: &policyv2.Policy{
|
||||
ACLs: []policyv2.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Protocol: "tcp",
|
||||
Sources: []policyv2.Alias{wildcard()},
|
||||
Destinations: []policyv2.AliasWithPorts{
|
||||
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
||||
},
|
||||
},
|
||||
},
|
||||
TagOwners: policyv2.TagOwners{
|
||||
policyv2.Tag("tag:test"): policyv2.Owners{usernameOwner("user1@test.no")},
|
||||
},
|
||||
},
|
||||
wantTag: true,
|
||||
},
|
||||
{
|
||||
name: "with-policy-username",
|
||||
policy: &policyv2.Policy{
|
||||
ACLs: []policyv2.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Protocol: "tcp",
|
||||
Sources: []policyv2.Alias{wildcard()},
|
||||
Destinations: []policyv2.AliasWithPorts{
|
||||
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
||||
},
|
||||
},
|
||||
},
|
||||
TagOwners: policyv2.TagOwners{
|
||||
policyv2.Tag("tag:test"): policyv2.Owners{usernameOwner("user1@")},
|
||||
},
|
||||
},
|
||||
wantTag: true,
|
||||
},
|
||||
{
|
||||
name: "with-policy-groups",
|
||||
policy: &policyv2.Policy{
|
||||
Groups: policyv2.Groups{
|
||||
policyv2.Group("group:admins"): []policyv2.Username{policyv2.Username("user1@")},
|
||||
},
|
||||
ACLs: []policyv2.ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Protocol: "tcp",
|
||||
Sources: []policyv2.Alias{wildcard()},
|
||||
Destinations: []policyv2.AliasWithPorts{
|
||||
aliasWithPorts(wildcard(), tailcfg.PortRangeAny),
|
||||
},
|
||||
},
|
||||
},
|
||||
TagOwners: policyv2.TagOwners{
|
||||
policyv2.Tag("tag:test"): policyv2.Owners{groupOwner("group:admins")},
|
||||
},
|
||||
},
|
||||
wantTag: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
spec := ScenarioSpec{
|
||||
NodesPerUser: 1,
|
||||
Users: []string{"user1"},
|
||||
}
|
||||
|
||||
scenario, err := NewScenario(spec)
|
||||
require.NoError(t, err)
|
||||
defer scenario.ShutdownAssertNoPanics(t)
|
||||
|
||||
err = scenario.CreateHeadscaleEnv(
|
||||
[]tsic.Option{tsic.WithTags([]string{"tag:test"})},
|
||||
hsic.WithTestName("cliadvtags"),
|
||||
hsic.WithACLPolicy(tt.policy),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
headscale, err := scenario.Headscale()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test list all nodes after added seconds
|
||||
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")
|
||||
}
|
||||
}
|
||||
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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeCommand(t *testing.T) {
|
||||
IntegrationSkip(t)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ type ControlServer interface {
|
||||
WaitForRunning() error
|
||||
CreateUser(user string) (*v1.User, error)
|
||||
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
|
||||
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*v1.PreAuthKey, error)
|
||||
DeleteAuthKey(user uint64, key string) error
|
||||
ListNodes(users ...string) ([]*v1.Node, error)
|
||||
DeleteNode(nodeID uint64) error
|
||||
@@ -32,6 +33,7 @@ type ControlServer interface {
|
||||
ListUsers() ([]*v1.User, error)
|
||||
MapUsers() (map[string]*v1.User, error)
|
||||
ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error)
|
||||
SetNodeTags(nodeID uint64, tags []string) error
|
||||
GetCert() []byte
|
||||
GetHostname() string
|
||||
GetIPInNetwork(network *dockertest.Network) string
|
||||
|
||||
@@ -1052,6 +1052,57 @@ func (t *HeadscaleInContainer) CreateAuthKey(
|
||||
return &preAuthKey, nil
|
||||
}
|
||||
|
||||
// CreateAuthKeyWithTags creates a new "authorisation key" for a User with the specified tags.
|
||||
// This is used to create tagged PreAuthKeys for testing the tags-as-identity model.
|
||||
func (t *HeadscaleInContainer) CreateAuthKeyWithTags(
|
||||
user uint64,
|
||||
reusable bool,
|
||||
ephemeral bool,
|
||||
tags []string,
|
||||
) (*v1.PreAuthKey, error) {
|
||||
command := []string{
|
||||
"headscale",
|
||||
"--user",
|
||||
strconv.FormatUint(user, 10),
|
||||
"preauthkeys",
|
||||
"create",
|
||||
"--expiration",
|
||||
"24h",
|
||||
"--output",
|
||||
"json",
|
||||
}
|
||||
|
||||
if reusable {
|
||||
command = append(command, "--reusable")
|
||||
}
|
||||
|
||||
if ephemeral {
|
||||
command = append(command, "--ephemeral")
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
command = append(command, "--tags", strings.Join(tags, ","))
|
||||
}
|
||||
|
||||
result, _, err := dockertestutil.ExecuteCommand(
|
||||
t.container,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute create auth key with tags command: %w", err)
|
||||
}
|
||||
|
||||
var preAuthKey v1.PreAuthKey
|
||||
|
||||
err = json.Unmarshal([]byte(result), &preAuthKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal auth key: %w", err)
|
||||
}
|
||||
|
||||
return &preAuthKey, nil
|
||||
}
|
||||
|
||||
// DeleteAuthKey deletes an "authorisation key" for a User.
|
||||
func (t *HeadscaleInContainer) DeleteAuthKey(
|
||||
user uint64,
|
||||
@@ -1369,6 +1420,36 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// SetNodeTags sets tags on a node via the headscale CLI.
|
||||
// This simulates what the Tailscale admin console UI does - it calls the headscale
|
||||
// SetTags API which is exposed via the CLI command: headscale nodes tag -i <id> -t <tags>.
|
||||
func (t *HeadscaleInContainer) SetNodeTags(nodeID uint64, tags []string) error {
|
||||
command := []string{
|
||||
"headscale", "nodes", "tag",
|
||||
"--identifier", strconv.FormatUint(nodeID, 10),
|
||||
"--output", "json",
|
||||
}
|
||||
|
||||
// Add tags - the CLI expects -t flag for each tag or comma-separated
|
||||
if len(tags) > 0 {
|
||||
command = append(command, "--tags", strings.Join(tags, ","))
|
||||
} else {
|
||||
// Empty tags to clear all tags
|
||||
command = append(command, "--tags", "")
|
||||
}
|
||||
|
||||
_, _, err := dockertestutil.ExecuteCommand(
|
||||
t.container,
|
||||
command,
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute set tags command (node %d, tags %v): %w", nodeID, tags, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteFile save file inside the Headscale container.
|
||||
func (t *HeadscaleInContainer) WriteFile(path string, data []byte) error {
|
||||
return integrationutil.WriteFileToContainer(t.pool, t.container, path, data)
|
||||
|
||||
@@ -2259,16 +2259,16 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
tsic.WithAcceptRoutes(),
|
||||
}
|
||||
|
||||
if tt.approver == "tag:approve" {
|
||||
tsOpts = append(tsOpts,
|
||||
tsic.WithTags([]string{"tag:approve"}),
|
||||
)
|
||||
}
|
||||
|
||||
route, err := scenario.SubnetOfNetwork("usernet1")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = scenario.createHeadscaleEnv(tt.withURL, tsOpts,
|
||||
// For authkey with tag approver, use tagged PreAuthKeys (tags-as-identity model)
|
||||
var preAuthKeyTags []string
|
||||
if !tt.withURL && strings.HasPrefix(tt.approver, "tag:") {
|
||||
preAuthKeyTags = []string{tt.approver}
|
||||
}
|
||||
|
||||
err = scenario.createHeadscaleEnvWithTags(tt.withURL, tsOpts, preAuthKeyTags,
|
||||
opts...,
|
||||
)
|
||||
requireNoErrHeadscaleEnv(t, err)
|
||||
@@ -2315,6 +2315,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// For webauth with tag approver, the node needs to advertise the tag during registration
|
||||
// (tags-as-identity model: webauth nodes can use --advertise-tags if authorized by tagOwners)
|
||||
if tt.withURL && strings.HasPrefix(tt.approver, "tag:") {
|
||||
tsOpts = append(tsOpts, tsic.WithTags([]string{tt.approver}))
|
||||
}
|
||||
|
||||
tsOpts = append(tsOpts, tsic.WithNetwork(usernet1))
|
||||
|
||||
// This whole dance is to add a node _after_ all the other nodes
|
||||
@@ -2349,7 +2355,14 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
userMap, err := headscale.MapUsers()
|
||||
require.NoError(t, err)
|
||||
|
||||
pak, err := scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
|
||||
// If the approver is a tag, create a tagged PreAuthKey
|
||||
// (tags-as-identity model: tags come from PreAuthKey, not --advertise-tags)
|
||||
var pak *v1.PreAuthKey
|
||||
if strings.HasPrefix(tt.approver, "tag:") {
|
||||
pak, err = scenario.CreatePreAuthKeyWithTags(userMap["user1"].GetId(), false, false, []string{tt.approver})
|
||||
} else {
|
||||
pak, err = scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.GetKey())
|
||||
@@ -2444,7 +2457,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.True(c, routerPeerFound, "Client should see the router peer")
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying routes sent to client after auto-approval")
|
||||
}, 30*time.Second, 200*time.Millisecond, "Verifying routes sent to client after auto-approval")
|
||||
|
||||
url := fmt.Sprintf("http://%s/etc/hostname", webip)
|
||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||
@@ -2453,7 +2466,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
result, err := client.Curl(url)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, result, 13)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying client can reach webservice through auto-approved route")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying client can reach webservice through auto-approved route")
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
@@ -2463,7 +2476,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying traceroute goes through auto-approved router")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying traceroute goes through auto-approved router")
|
||||
|
||||
// Remove the auto approval from the policy, any routes already enabled should be allowed.
|
||||
prefix = *route
|
||||
@@ -2506,7 +2519,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
}
|
||||
}
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying routes remain after policy change")
|
||||
}, 30*time.Second, 200*time.Millisecond, "Verifying routes remain after policy change")
|
||||
|
||||
url = fmt.Sprintf("http://%s/etc/hostname", webip)
|
||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||
@@ -2515,7 +2528,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
result, err := client.Curl(url)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, result, 13)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying client can still reach webservice after policy change")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying client can still reach webservice after policy change")
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
@@ -2525,7 +2538,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying traceroute still goes through router after policy change")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying traceroute still goes through router after policy change")
|
||||
|
||||
// Disable the route, making it unavailable since it is no longer auto-approved
|
||||
_, err = headscale.ApproveRoutes(
|
||||
@@ -2541,7 +2554,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
requireNodeRouteCountWithCollect(c, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 0, 0)
|
||||
}, 10*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
}, 15*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
|
||||
// Verify that the routes have been sent to the client.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -2552,7 +2565,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
peerStatus := status.Peer[peerKey]
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
}
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying routes disabled after route removal")
|
||||
}, 30*time.Second, 200*time.Millisecond, "Verifying routes disabled after route removal")
|
||||
|
||||
// Add the route back to the auto approver in the policy, the route should
|
||||
// now become available again.
|
||||
@@ -2580,7 +2593,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
nodes, err = headscale.ListNodes()
|
||||
assert.NoError(c, err)
|
||||
requireNodeRouteCountWithCollect(c, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
|
||||
}, 10*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
}, 15*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
|
||||
// Verify that the routes have been sent to the client.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -2600,7 +2613,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
}
|
||||
}
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying routes re-enabled after policy re-approval")
|
||||
}, 30*time.Second, 200*time.Millisecond, "Verifying routes re-enabled after policy re-approval")
|
||||
|
||||
url = fmt.Sprintf("http://%s/etc/hostname", webip)
|
||||
t.Logf("url from %s to %s", client.Hostname(), url)
|
||||
@@ -2609,7 +2622,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
result, err := client.Curl(url)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, result, 13)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying client can reach webservice after route re-approval")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying client can reach webservice after route re-approval")
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := client.Traceroute(webip)
|
||||
@@ -2619,7 +2632,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying traceroute goes through router after re-approval")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying traceroute goes through router after re-approval")
|
||||
|
||||
// Advertise and validate a subnet of an auto approved route, /24 inside the
|
||||
// auto approved /16.
|
||||
@@ -2639,7 +2652,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
assert.NoError(c, err)
|
||||
requireNodeRouteCountWithCollect(c, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
|
||||
requireNodeRouteCountWithCollect(c, nodes[1], 1, 1, 1)
|
||||
}, 10*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
}, 15*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
|
||||
// Verify that the routes have been sent to the client.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -2663,7 +2676,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
}
|
||||
}
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying sub-route propagated to client")
|
||||
}, 30*time.Second, 200*time.Millisecond, "Verifying sub-route propagated to client")
|
||||
|
||||
// Advertise a not approved route will not end up anywhere
|
||||
command = []string{
|
||||
@@ -2683,7 +2696,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
requireNodeRouteCountWithCollect(c, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
|
||||
requireNodeRouteCountWithCollect(c, nodes[1], 1, 1, 0)
|
||||
requireNodeRouteCountWithCollect(c, nodes[2], 0, 0, 0)
|
||||
}, 10*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
}, 15*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
|
||||
// Verify that the routes have been sent to the client.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -2703,7 +2716,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
}
|
||||
}
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying unapproved route not propagated")
|
||||
}, 30*time.Second, 200*time.Millisecond, "Verifying unapproved route not propagated")
|
||||
|
||||
// Exit routes are also automatically approved
|
||||
command = []string{
|
||||
@@ -2721,7 +2734,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
requireNodeRouteCountWithCollect(c, MustFindNode(routerUsernet1.Hostname(), nodes), 1, 1, 1)
|
||||
requireNodeRouteCountWithCollect(c, nodes[1], 1, 1, 0)
|
||||
requireNodeRouteCountWithCollect(c, nodes[2], 2, 2, 2)
|
||||
}, 10*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
}, 15*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
|
||||
// Verify that the routes have been sent to the client.
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -2742,7 +2755,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
|
||||
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
|
||||
}
|
||||
}
|
||||
}, 5*time.Second, 200*time.Millisecond, "Verifying exit node routes propagated to client")
|
||||
}, 30*time.Second, 200*time.Millisecond, "Verifying exit node routes propagated to client")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2985,7 +2998,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
||||
|
||||
// Check that the router has 3 routes now approved and available
|
||||
requireNodeRouteCountWithCollect(c, routerNode, 3, 3, 3)
|
||||
}, 10*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
}, 15*time.Second, 500*time.Millisecond, "route state changes should propagate")
|
||||
|
||||
// Now check the client node status
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
@@ -3006,7 +3019,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
||||
result, err := nodeClient.Curl(weburl)
|
||||
assert.NoError(c, err)
|
||||
assert.Len(c, result, 13)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying node can reach webservice through allowed route")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying node can reach webservice through allowed route")
|
||||
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
tr, err := nodeClient.Traceroute(webip)
|
||||
@@ -3016,5 +3029,5 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
|
||||
return
|
||||
}
|
||||
assertTracerouteViaIPWithCollect(c, tr, ip)
|
||||
}, 20*time.Second, 200*time.Millisecond, "Verifying traceroute goes through router")
|
||||
}, 60*time.Second, 200*time.Millisecond, "Verifying traceroute goes through router")
|
||||
}
|
||||
|
||||
@@ -473,6 +473,27 @@ func (s *Scenario) CreatePreAuthKey(
|
||||
return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable)
|
||||
}
|
||||
|
||||
// CreatePreAuthKeyWithTags creates a "pre authorised key" with the specified tags
|
||||
// to be created in the Headscale instance on behalf of the Scenario.
|
||||
func (s *Scenario) CreatePreAuthKeyWithTags(
|
||||
user uint64,
|
||||
reusable bool,
|
||||
ephemeral bool,
|
||||
tags []string,
|
||||
) (*v1.PreAuthKey, error) {
|
||||
headscale, err := s.Headscale()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create preauth key with tags: %w", errNoHeadscaleAvailable)
|
||||
}
|
||||
|
||||
key, err := headscale.CreateAuthKeyWithTags(user, reusable, ephemeral, tags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create preauth key with tags: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// CreateUser creates a User to be created in the
|
||||
// Headscale instance on behalf of the Scenario.
|
||||
func (s *Scenario) CreateUser(user string) (*v1.User, error) {
|
||||
@@ -767,6 +788,19 @@ func (s *Scenario) createHeadscaleEnv(
|
||||
withURL bool,
|
||||
tsOpts []tsic.Option,
|
||||
opts ...hsic.Option,
|
||||
) error {
|
||||
return s.createHeadscaleEnvWithTags(withURL, tsOpts, nil, opts...)
|
||||
}
|
||||
|
||||
// createHeadscaleEnvWithTags starts the headscale environment and the clients
|
||||
// according to the ScenarioSpec passed to the Scenario. If preAuthKeyTags is
|
||||
// non-empty and withURL is false, the tags will be applied to the PreAuthKey
|
||||
// (tags-as-identity model).
|
||||
func (s *Scenario) createHeadscaleEnvWithTags(
|
||||
withURL bool,
|
||||
tsOpts []tsic.Option,
|
||||
preAuthKeyTags []string,
|
||||
opts ...hsic.Option,
|
||||
) error {
|
||||
headscale, err := s.Headscale(opts...)
|
||||
if err != nil {
|
||||
@@ -797,7 +831,13 @@ func (s *Scenario) createHeadscaleEnv(
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
key, err := s.CreatePreAuthKey(u.GetId(), true, false)
|
||||
// Use tagged PreAuthKey if tags are provided (tags-as-identity model)
|
||||
var key *v1.PreAuthKey
|
||||
if len(preAuthKeyTags) > 0 {
|
||||
key, err = s.CreatePreAuthKeyWithTags(u.GetId(), true, false, preAuthKeyTags)
|
||||
} else {
|
||||
key, err = s.CreatePreAuthKey(u.GetId(), true, false)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
2465
integration/tags_test.go
Normal file
2465
integration/tags_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user