all: fix golangci-lint issues (#3064)

This commit is contained in:
Kristoffer Dalby 2026-02-06 21:45:32 +01:00 committed by GitHub
parent bfb6fd80df
commit ce580f8245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
131 changed files with 3131 additions and 1560 deletions

View file

@ -1877,7 +1877,7 @@ func TestACLAutogroupSelf(t *testing.T) {
result, err := client.Curl(url)
assert.Empty(t, result, "user1 should not be able to access user2's regular devices (autogroup:self isolation)")
assert.Error(t, err, "connection from user1 to user2 regular device should fail")
require.Error(t, err, "connection from user1 to user2 regular device should fail")
}
}
@ -1896,6 +1896,7 @@ func TestACLAutogroupSelf(t *testing.T) {
}
}
//nolint:gocyclo // complex integration test scenario
func TestACLPolicyPropagationOverTime(t *testing.T) {
IntegrationSkip(t)

View file

@ -1,6 +1,7 @@
package integration
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
@ -35,6 +36,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -46,6 +48,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
// Create an API key using the CLI
var validAPIKey string
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
apiKeyOutput, err := headscale.Execute(
[]string{
@ -63,7 +66,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
// Get the API endpoint
endpoint := headscale.GetEndpoint()
apiURL := fmt.Sprintf("%s/api/v1/user", endpoint)
apiURL := endpoint + "/api/v1/user"
// Create HTTP client
client := &http.Client{
@ -76,11 +79,12 @@ func TestAPIAuthenticationBypass(t *testing.T) {
t.Run("HTTP_NoAuthHeader", func(t *testing.T) {
// Test 1: Request without any Authorization header
// Expected: Should return 401 with ONLY "Unauthorized" text, no user data
req, err := http.NewRequest("GET", apiURL, nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
@ -99,6 +103,7 @@ func TestAPIAuthenticationBypass(t *testing.T) {
// Should NOT contain user data after "Unauthorized"
// This is the security bypass - if users array is present, auth was bypassed
var jsonCheck map[string]any
jsonErr := json.Unmarshal(body, &jsonCheck)
// If we can unmarshal JSON and it contains "users", that's the bypass
@ -126,12 +131,13 @@ func TestAPIAuthenticationBypass(t *testing.T) {
t.Run("HTTP_InvalidAuthHeader", func(t *testing.T) {
// Test 2: Request with invalid Authorization header (missing "Bearer " prefix)
// Expected: Should return 401 with ONLY "Unauthorized" text, no user data
req, err := http.NewRequest("GET", apiURL, nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
require.NoError(t, err)
req.Header.Set("Authorization", "InvalidToken")
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
@ -159,12 +165,13 @@ func TestAPIAuthenticationBypass(t *testing.T) {
// Test 3: Request with Bearer prefix but invalid token
// Expected: Should return 401 with ONLY "Unauthorized" text, no user data
// Note: Both malformed and properly formatted invalid tokens should return 401
req, err := http.NewRequest("GET", apiURL, nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer invalid-token-12345")
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
@ -191,12 +198,13 @@ func TestAPIAuthenticationBypass(t *testing.T) {
t.Run("HTTP_ValidAPIKey", func(t *testing.T) {
// Test 4: Request with valid API key
// Expected: Should return 200 with user data (this is the authorized case)
req, err := http.NewRequest("GET", apiURL, nil)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, apiURL, nil)
require.NoError(t, err)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", validAPIKey))
req.Header.Set("Authorization", "Bearer "+validAPIKey)
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
@ -208,16 +216,19 @@ func TestAPIAuthenticationBypass(t *testing.T) {
// Should be able to parse as protobuf JSON
var response v1.ListUsersResponse
err = protojson.Unmarshal(body, &response)
assert.NoError(t, err, "Response should be valid protobuf JSON with valid API key")
require.NoError(t, err, "Response should be valid protobuf JSON with valid API key")
// Should contain our test users
users := response.GetUsers()
assert.Len(t, users, 3, "Should have 3 users")
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
}
assert.Contains(t, userNames, "user1")
assert.Contains(t, userNames, "user2")
assert.Contains(t, userNames, "user3")
@ -234,6 +245,7 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -254,10 +266,11 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
},
)
require.NoError(t, err)
validAPIKey := strings.TrimSpace(apiKeyOutput)
endpoint := headscale.GetEndpoint()
apiURL := fmt.Sprintf("%s/api/v1/user", endpoint)
apiURL := endpoint + "/api/v1/user"
t.Run("Curl_NoAuth", func(t *testing.T) {
// Execute curl from inside the headscale container without auth
@ -274,17 +287,24 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
// Parse the output
lines := strings.Split(curlOutput, "\n")
var httpCode string
var responseBody string
var (
httpCode string
responseBody string
)
var responseBodySb280 strings.Builder
for _, line := range lines {
if after, ok := strings.CutPrefix(line, "HTTP_CODE:"); ok {
httpCode = after
} else {
responseBody += line
responseBodySb280.WriteString(line)
}
}
responseBody += responseBodySb280.String()
// Should return 401
assert.Equal(t, "401", httpCode,
"Curl without auth should return 401")
@ -320,17 +340,24 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
require.NoError(t, err)
lines := strings.Split(curlOutput, "\n")
var httpCode string
var responseBody string
var (
httpCode string
responseBody string
)
var responseBodySb326 strings.Builder
for _, line := range lines {
if after, ok := strings.CutPrefix(line, "HTTP_CODE:"); ok {
httpCode = after
} else {
responseBody += line
responseBodySb326.WriteString(line)
}
}
responseBody += responseBodySb326.String()
assert.Equal(t, "401", httpCode)
assert.Contains(t, responseBody, "Unauthorized")
assert.NotContains(t, responseBody, "testuser1",
@ -346,7 +373,7 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
"curl",
"-s",
"-H",
fmt.Sprintf("Authorization: Bearer %s", validAPIKey),
"Authorization: Bearer " + validAPIKey,
"-w",
"\nHTTP_CODE:%{http_code}",
apiURL,
@ -355,25 +382,34 @@ func TestAPIAuthenticationBypassCurl(t *testing.T) {
require.NoError(t, err)
lines := strings.Split(curlOutput, "\n")
var httpCode string
var responseBody string
var (
httpCode string
responseBody string
)
var responseBodySb361 strings.Builder
for _, line := range lines {
if after, ok := strings.CutPrefix(line, "HTTP_CODE:"); ok {
httpCode = after
} else {
responseBody += line
responseBodySb361.WriteString(line)
}
}
responseBody += responseBodySb361.String()
// Should succeed
assert.Equal(t, "200", httpCode,
"Curl with valid API key should return 200")
// Should contain user data
var response v1.ListUsersResponse
err = protojson.Unmarshal([]byte(responseBody), &response)
assert.NoError(t, err, "Response should be valid protobuf JSON")
require.NoError(t, err, "Response should be valid protobuf JSON")
users := response.GetUsers()
assert.Len(t, users, 2, "Should have 2 users")
})
@ -391,6 +427,7 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -420,11 +457,12 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
},
)
require.NoError(t, err)
validAPIKey := strings.TrimSpace(apiKeyOutput)
// Get the gRPC endpoint
// For gRPC, we need to use the hostname and port 50443
grpcAddress := fmt.Sprintf("%s:50443", headscale.GetHostname())
grpcAddress := headscale.GetHostname() + ":50443"
t.Run("gRPC_NoAPIKey", func(t *testing.T) {
// Test 1: Try to use CLI without API key (should fail)
@ -452,7 +490,7 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
)
// Should fail with authentication error
assert.Error(t, err,
require.Error(t, err,
"gRPC connection with invalid API key should fail")
// Should contain authentication error message
@ -481,20 +519,22 @@ func TestGRPCAuthenticationBypass(t *testing.T) {
)
// Should succeed
assert.NoError(t, err,
require.NoError(t, err,
"gRPC connection with valid API key should succeed, output: %s", output)
// CLI outputs the users array directly, not wrapped in ListUsersResponse
// Parse as JSON array (CLI uses json.Marshal, not protojson)
var users []*v1.User
err = json.Unmarshal([]byte(output), &users)
assert.NoError(t, err, "Response should be valid JSON array")
require.NoError(t, err, "Response should be valid JSON array")
assert.Len(t, users, 2, "Should have 2 users")
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
}
assert.Contains(t, userNames, "grpcuser1")
assert.Contains(t, userNames, "grpcuser2")
})
@ -513,6 +553,7 @@ func TestCLIWithConfigAuthenticationBypass(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -540,9 +581,10 @@ func TestCLIWithConfigAuthenticationBypass(t *testing.T) {
},
)
require.NoError(t, err)
validAPIKey := strings.TrimSpace(apiKeyOutput)
grpcAddress := fmt.Sprintf("%s:50443", headscale.GetHostname())
grpcAddress := headscale.GetHostname() + ":50443"
// Create a config file for testing
configWithoutKey := fmt.Sprintf(`
@ -602,7 +644,7 @@ cli:
)
// Should fail
assert.Error(t, err,
require.Error(t, err,
"CLI with invalid API key should fail")
// Should indicate authentication failure
@ -637,20 +679,22 @@ cli:
)
// Should succeed
assert.NoError(t, err,
require.NoError(t, err,
"CLI with valid API key should succeed")
// CLI outputs the users array directly, not wrapped in ListUsersResponse
// Parse as JSON array (CLI uses json.Marshal, not protojson)
var users []*v1.User
err = json.Unmarshal([]byte(output), &users)
assert.NoError(t, err, "Response should be valid JSON array")
require.NoError(t, err, "Response should be valid JSON array")
assert.Len(t, users, 2, "Should have 2 users")
userNames := make([]string, len(users))
for i, u := range users {
userNames[i] = u.GetName()
}
assert.Contains(t, userNames, "cliuser1")
assert.Contains(t, userNames, "cliuser2")
})

View file

@ -31,6 +31,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -69,18 +70,24 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
// assertClientsState(t, allClients)
clientIPs := make(map[TailscaleClient][]netip.Addr)
for _, client := range allClients {
ips, err := client.IPs()
if err != nil {
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
}
clientIPs[client] = ips
}
var listNodes []*v1.Node
var nodeCountBeforeLogout int
var (
listNodes []*v1.Node
nodeCountBeforeLogout int
)
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, listNodes, len(allClients))
@ -111,6 +118,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
t.Logf("Validating node persistence after logout at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err, "Failed to list nodes after logout")
assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should match before logout count - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes))
@ -148,6 +156,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
t.Logf("Validating node persistence after relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err, "Failed to list nodes after relogin")
assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should remain unchanged after relogin - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes))
@ -201,6 +210,7 @@ func TestAuthKeyLogoutAndReloginSameUser(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, listNodes, nodeCountBeforeLogout)
@ -255,10 +265,14 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
requireAllClientsOnline(t, headscale, expectedNodes, true, "all clients should be connected after initial login", 120*time.Second)
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
var listNodes []*v1.Node
var nodeCountBeforeLogout int
var (
listNodes []*v1.Node
nodeCountBeforeLogout int
)
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, listNodes, len(allClients))
@ -301,9 +315,11 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
}
var user1Nodes []*v1.Node
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
user1Nodes, err = headscale.ListNodes("user1")
assert.NoError(ct, err, "Failed to list nodes for user1 after relogin")
assert.Len(ct, user1Nodes, len(allClients), "User1 should have all %d clients after relogin, got %d nodes", len(allClients), len(user1Nodes))
@ -323,21 +339,24 @@ func TestAuthKeyLogoutAndReloginNewUser(t *testing.T) {
// When nodes re-authenticate with a different user's pre-auth key, NEW nodes are created
// for the new user. The original nodes remain with the original user.
var user2Nodes []*v1.Node
t.Logf("Validating user2 node persistence after user1 relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
user2Nodes, err = headscale.ListNodes("user2")
assert.NoError(ct, err, "Failed to list nodes for user2 after user1 relogin")
assert.Len(ct, user2Nodes, len(allClients)/2, "User2 should still have %d clients after user1 relogin, got %d nodes", len(allClients)/2, len(user2Nodes))
}, 30*time.Second, 2*time.Second, "validating user2 nodes persist after user1 relogin (should not be affected)")
t.Logf("Validating client login states after user switch at %s", time.Now().Format(TimestampFormat))
for _, client := range allClients {
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
status, err := client.Status()
assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname())
assert.Equal(ct, "user1@test.no", status.User[status.Self.UserID].LoginName, "Client %s should be logged in as user1 after user switch, got %s", client.Hostname(), status.User[status.Self.UserID].LoginName)
}, 30*time.Second, 2*time.Second, fmt.Sprintf("validating %s is logged in as user1 after auth key user switch", client.Hostname()))
}, 30*time.Second, 2*time.Second, "validating %s is logged in as user1 after auth key user switch", client.Hostname())
}
}
@ -352,6 +371,7 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -377,11 +397,13 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
// assertClientsState(t, allClients)
clientIPs := make(map[TailscaleClient][]netip.Addr)
for _, client := range allClients {
ips, err := client.IPs()
if err != nil {
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
}
clientIPs[client] = ips
}
@ -395,10 +417,14 @@ func TestAuthKeyLogoutAndReloginSameUserExpiredKey(t *testing.T) {
requireAllClientsOnline(t, headscale, expectedNodes, true, "all clients should be connected after initial login", 120*time.Second)
requireAllClientsNetInfoAndDERP(t, headscale, expectedNodes, "all clients should have NetInfo and DERP after initial login", 3*time.Minute)
var listNodes []*v1.Node
var nodeCountBeforeLogout int
var (
listNodes []*v1.Node
nodeCountBeforeLogout int
)
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, listNodes, len(allClients))

View file

@ -148,6 +148,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -175,6 +176,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
syncCompleteTime := time.Now()
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
loginDuration := time.Since(syncCompleteTime)
t.Logf("Login and sync completed in %v", loginDuration)
@ -206,6 +208,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) {
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
// Check each client's status individually to provide better diagnostics
expiredCount := 0
for _, client := range allClients {
status, err := client.Status()
if assert.NoError(ct, err, "failed to get status for client %s", client.Hostname()) {
@ -355,6 +358,7 @@ func TestOIDC024UserCreation(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -412,6 +416,7 @@ func TestOIDCAuthenticationWithPKCE(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -469,6 +474,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
oidcMockUser("user1", true),
},
})
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -507,6 +513,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
listUsers, err := headscale.ListUsers()
assert.NoError(ct, err, "Failed to list users during initial validation")
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
wantUsers := []*v1.User{
{
Id: 1,
@ -527,9 +534,12 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
}, 30*time.Second, 1*time.Second, "validating user1 creation after initial OIDC login")
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
var listNodes []*v1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err, "Failed to list nodes during initial validation")
assert.Len(ct, listNodes, 1, "Expected exactly 1 node after first login, got %d", len(listNodes))
@ -537,14 +547,19 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Collect expected node IDs for validation after user1 initial login
expectedNodes := make([]types.NodeID, 0, 1)
var nodeID uint64
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
status := ts.MustStatus()
assert.NotEmpty(ct, status.Self.ID, "Node ID should be populated in status")
var err error
nodeID, err = strconv.ParseUint(string(status.Self.ID), 10, 64)
assert.NoError(ct, err, "Failed to parse node ID from status")
}, 30*time.Second, 1*time.Second, "waiting for node ID to be populated in status after initial login")
expectedNodes = append(expectedNodes, types.NodeID(nodeID))
// Validate initial connection state for user1
@ -582,6 +597,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
listUsers, err := headscale.ListUsers()
assert.NoError(ct, err, "Failed to list users after user2 login")
assert.Len(ct, listUsers, 2, "Expected exactly 2 users after user2 login, got %d users", len(listUsers))
wantUsers := []*v1.User{
{
Id: 1,
@ -637,10 +653,12 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Security validation: Only user2's node should be active after user switch
var activeUser2NodeID types.NodeID
for _, node := range listNodesAfterNewUserLogin {
if node.GetUser().GetId() == 2 { // user2
activeUser2NodeID = types.NodeID(node.GetId())
t.Logf("Active user2 node: %d (User: %s)", node.GetId(), node.GetUser().GetName())
break
}
}
@ -654,6 +672,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Check user2 node is online
if node, exists := nodeStore[activeUser2NodeID]; exists {
assert.NotNil(c, node.IsOnline, "User2 node should have online status")
if node.IsOnline != nil {
assert.True(c, *node.IsOnline, "User2 node should be online after login")
}
@ -746,6 +765,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
listUsers, err := headscale.ListUsers()
assert.NoError(ct, err, "Failed to list users during final validation")
assert.Len(ct, listUsers, 2, "Should still have exactly 2 users after user1 relogin, got %d", len(listUsers))
wantUsers := []*v1.User{
{
Id: 1,
@ -815,10 +835,12 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Security validation: Only user1's node should be active after relogin
var activeUser1NodeID types.NodeID
for _, node := range listNodesAfterLoggingBackIn {
if node.GetUser().GetId() == 1 { // user1
activeUser1NodeID = types.NodeID(node.GetId())
t.Logf("Active user1 node after relogin: %d (User: %s)", node.GetId(), node.GetUser().GetName())
break
}
}
@ -832,6 +854,7 @@ func TestOIDCReloginSameNodeNewUser(t *testing.T) {
// Check user1 node is online
if node, exists := nodeStore[activeUser1NodeID]; exists {
assert.NotNil(c, node.IsOnline, "User1 node should have online status after relogin")
if node.IsOnline != nil {
assert.True(c, *node.IsOnline, "User1 node should be online after relogin")
}
@ -906,6 +929,7 @@ func TestOIDCFollowUpUrl(t *testing.T) {
time.Sleep(2 * time.Minute)
var newUrl *url.URL
assert.EventuallyWithT(t, func(c *assert.CollectT) {
st, err := ts.Status()
assert.NoError(c, err)
@ -1029,7 +1053,7 @@ func TestOIDCMultipleOpenedLoginUrls(t *testing.T) {
require.NotEqual(t, redirect1.String(), redirect2.String())
// complete auth with the first opened "browser tab"
_, redirect1, err = doLoginURLWithClient(ts.Hostname(), redirect1, loginClient, true)
_, _, err = doLoginURLWithClient(ts.Hostname(), redirect1, loginClient, true)
require.NoError(t, err)
listUsers, err = headscale.ListUsers()
@ -1106,6 +1130,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
oidcMockUser("user1", true), // Relogin with same user
},
})
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -1145,6 +1170,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
listUsers, err := headscale.ListUsers()
assert.NoError(ct, err, "Failed to list users during initial validation")
assert.Len(ct, listUsers, 1, "Expected exactly 1 user after first login, got %d", len(listUsers))
wantUsers := []*v1.User{
{
Id: 1,
@ -1165,9 +1191,12 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
}, 30*time.Second, 1*time.Second, "validating user1 creation after initial OIDC login")
t.Logf("Validating initial node creation at %s", time.Now().Format(TimestampFormat))
var initialNodes []*v1.Node
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
initialNodes, err = headscale.ListNodes()
assert.NoError(ct, err, "Failed to list nodes during initial validation")
assert.Len(ct, initialNodes, 1, "Expected exactly 1 node after first login, got %d", len(initialNodes))
@ -1175,14 +1204,19 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
// Collect expected node IDs for validation after user1 initial login
expectedNodes := make([]types.NodeID, 0, 1)
var nodeID uint64
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
status := ts.MustStatus()
assert.NotEmpty(ct, status.Self.ID, "Node ID should be populated in status")
var err error
nodeID, err = strconv.ParseUint(string(status.Self.ID), 10, 64)
assert.NoError(ct, err, "Failed to parse node ID from status")
}, 30*time.Second, 1*time.Second, "waiting for node ID to be populated in status after initial login")
expectedNodes = append(expectedNodes, types.NodeID(nodeID))
// Validate initial connection state for user1
@ -1239,6 +1273,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
listUsers, err := headscale.ListUsers()
assert.NoError(ct, err, "Failed to list users during final validation")
assert.Len(ct, listUsers, 1, "Should still have exactly 1 user after same-user relogin, got %d", len(listUsers))
wantUsers := []*v1.User{
{
Id: 1,
@ -1259,6 +1294,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
}, 30*time.Second, 1*time.Second, "validating user1 persistence after same-user OIDC relogin cycle")
var finalNodes []*v1.Node
t.Logf("Final node validation: checking node stability after same-user relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
finalNodes, err = headscale.ListNodes()
@ -1282,6 +1318,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
// Security validation: user1's node should be active after relogin
activeUser1NodeID := types.NodeID(finalNodes[0].GetId())
t.Logf("Validating user1 node is online after same-user relogin at %s", time.Now().Format(TimestampFormat))
require.EventuallyWithT(t, func(c *assert.CollectT) {
nodeStore, err := headscale.DebugNodeStore()
@ -1290,6 +1327,7 @@ func TestOIDCReloginSameNodeSameUser(t *testing.T) {
// Check user1 node is online
if node, exists := nodeStore[activeUser1NodeID]; exists {
assert.NotNil(c, node.IsOnline, "User1 node should have online status after same-user relogin")
if node.IsOnline != nil {
assert.True(c, *node.IsOnline, "User1 node should be online after same-user relogin")
}
@ -1359,6 +1397,7 @@ func TestOIDCExpiryAfterRestart(t *testing.T) {
// Verify initial expiry is set
var initialExpiry time.Time
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
nodes, err := headscale.ListNodes()
assert.NoError(ct, err)

View file

@ -1,7 +1,6 @@
package integration
import (
"fmt"
"net/netip"
"slices"
"testing"
@ -67,6 +66,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -106,22 +106,27 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
validateInitialConnection(t, headscale, expectedNodes)
var listNodes []*v1.Node
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err, "Failed to list nodes after web authentication")
assert.Len(ct, listNodes, len(allClients), "Expected %d nodes after web auth, got %d", len(allClients), len(listNodes))
}, 30*time.Second, 2*time.Second, "validating node count matches client count after web authentication")
nodeCountBeforeLogout := len(listNodes)
t.Logf("node count before logout: %d", nodeCountBeforeLogout)
clientIPs := make(map[TailscaleClient][]netip.Addr)
for _, client := range allClients {
ips, err := client.IPs()
if err != nil {
t.Fatalf("failed to get IPs for client %s: %s", client.Hostname(), err)
}
clientIPs[client] = ips
}
@ -152,6 +157,7 @@ func TestAuthWebFlowLogoutAndReloginSameUser(t *testing.T) {
t.Logf("Validating node persistence after logout at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err, "Failed to list nodes after web flow logout")
assert.Len(ct, listNodes, nodeCountBeforeLogout, "Node count should remain unchanged after logout - expected %d nodes, got %d", nodeCountBeforeLogout, len(listNodes))
@ -226,6 +232,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -240,9 +247,13 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
allClients, err := scenario.ListTailscaleClients()
requireNoErrListClients(t, err)
allIps, err := scenario.ListTailscaleClientsIPs()
var allIps []netip.Addr
allIps, err = scenario.ListTailscaleClientsIPs()
requireNoErrListClientIPs(t, err)
_ = allIps // used below after user switch
err = scenario.WaitForTailscaleSync()
requireNoErrSync(t, err)
@ -256,13 +267,16 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
validateInitialConnection(t, headscale, expectedNodes)
var listNodes []*v1.Node
t.Logf("Validating initial node count after web auth at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
listNodes, err = headscale.ListNodes()
assert.NoError(ct, err, "Failed to list nodes after initial web authentication")
assert.Len(ct, listNodes, len(allClients), "Expected %d nodes after web auth, got %d", len(allClients), len(listNodes))
}, 30*time.Second, 2*time.Second, "validating node count matches client count after initial web authentication")
nodeCountBeforeLogout := len(listNodes)
t.Logf("node count before logout: %d", nodeCountBeforeLogout)
@ -299,7 +313,7 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Register all clients as user1 (this is where cross-user registration happens)
// This simulates: headscale nodes register --user user1 --key <key>
scenario.runHeadscaleRegister("user1", body)
_ = scenario.runHeadscaleRegister("user1", body)
}
// Wait for all clients to reach running state
@ -313,9 +327,11 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
t.Logf("all clients logged back in as user1")
var user1Nodes []*v1.Node
t.Logf("Validating user1 node count after relogin at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
user1Nodes, err = headscale.ListNodes("user1")
assert.NoError(ct, err, "Failed to list nodes for user1 after web flow relogin")
assert.Len(ct, user1Nodes, len(allClients), "User1 should have all %d clients after web flow relogin, got %d nodes", len(allClients), len(user1Nodes))
@ -333,21 +349,24 @@ func TestAuthWebFlowLogoutAndReloginNewUser(t *testing.T) {
// Validate that user2's old nodes still exist in database (but are expired/offline)
// When CLI registration creates new nodes for user1, user2's old nodes remain
var user2Nodes []*v1.Node
t.Logf("Validating user2 old nodes remain in database after CLI registration to user1 at %s", time.Now().Format(TimestampFormat))
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
user2Nodes, err = headscale.ListNodes("user2")
assert.NoError(ct, err, "Failed to list nodes for user2 after CLI registration to user1")
assert.Len(ct, user2Nodes, len(allClients)/2, "User2 should still have %d old nodes (likely expired) after CLI registration to user1, got %d nodes", len(allClients)/2, len(user2Nodes))
}, 30*time.Second, 2*time.Second, "validating user2 old nodes remain in database after CLI registration to user1")
t.Logf("Validating client login states after web flow user switch at %s", time.Now().Format(TimestampFormat))
for _, client := range allClients {
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
status, err := client.Status()
assert.NoError(ct, err, "Failed to get status for client %s", client.Hostname())
assert.Equal(ct, "user1@test.no", status.User[status.Self.UserID].LoginName, "Client %s should be logged in as user1 after web flow user switch, got %s", client.Hostname(), status.User[status.Self.UserID].LoginName)
}, 30*time.Second, 2*time.Second, fmt.Sprintf("validating %s is logged in as user1 after web flow user switch", client.Hostname()))
}, 30*time.Second, 2*time.Second, "validating %s is logged in as user1 after web flow user switch", client.Hostname())
}
// Test connectivity after user switch

View file

@ -203,7 +203,7 @@ func TestUserCommand(t *testing.T) {
"--identifier=1",
},
)
assert.NoError(t, err)
require.NoError(t, err)
assert.Contains(t, deleteResult, "User destroyed")
var listAfterIDDelete []*v1.User
@ -245,7 +245,7 @@ func TestUserCommand(t *testing.T) {
"--name=newname",
},
)
assert.NoError(t, err)
require.NoError(t, err)
assert.Contains(t, deleteResult, "User destroyed")
var listAfterNameDelete []v1.User
@ -571,7 +571,9 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) {
func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) {
IntegrationSkip(t)
//nolint:goconst // test data, not worth extracting
user1 := "user1"
//nolint:goconst // test data, not worth extracting
user2 := "user2"
spec := ScenarioSpec{
@ -829,7 +831,7 @@ func TestApiKeyCommand(t *testing.T) {
"json",
},
)
assert.NoError(t, err)
require.NoError(t, err)
assert.NotEmpty(t, apiResult)
keys[idx] = apiResult
@ -907,7 +909,7 @@ func TestApiKeyCommand(t *testing.T) {
listedAPIKeys[idx].GetPrefix(),
},
)
assert.NoError(t, err)
require.NoError(t, err)
expiredPrefixes[listedAPIKeys[idx].GetPrefix()] = true
}
@ -952,7 +954,7 @@ func TestApiKeyCommand(t *testing.T) {
"--prefix",
listedAPIKeys[0].GetPrefix(),
})
assert.NoError(t, err)
require.NoError(t, err)
var listedAPIKeysAfterDelete []v1.ApiKey
@ -1071,7 +1073,7 @@ func TestNodeCommand(t *testing.T) {
}
nodes := make([]*v1.Node, len(regIDs))
assert.NoError(t, err)
require.NoError(t, err)
for index, regID := range regIDs {
_, err := headscale.Execute(
@ -1089,7 +1091,7 @@ func TestNodeCommand(t *testing.T) {
"json",
},
)
assert.NoError(t, err)
require.NoError(t, err)
var node v1.Node
@ -1156,7 +1158,7 @@ func TestNodeCommand(t *testing.T) {
}
otherUserMachines := make([]*v1.Node, len(otherUserRegIDs))
assert.NoError(t, err)
require.NoError(t, err)
for index, regID := range otherUserRegIDs {
_, err := headscale.Execute(
@ -1174,7 +1176,7 @@ func TestNodeCommand(t *testing.T) {
"json",
},
)
assert.NoError(t, err)
require.NoError(t, err)
var node v1.Node
@ -1281,7 +1283,7 @@ func TestNodeCommand(t *testing.T) {
"--force",
},
)
assert.NoError(t, err)
require.NoError(t, err)
// Test: list main user after node is deleted
var listOnlyMachineUserAfterDelete []v1.Node
@ -1348,7 +1350,7 @@ func TestNodeExpireCommand(t *testing.T) {
"json",
},
)
assert.NoError(t, err)
require.NoError(t, err)
var node v1.Node
@ -1411,7 +1413,7 @@ func TestNodeExpireCommand(t *testing.T) {
strconv.FormatUint(listAll[idx].GetId(), 10),
},
)
assert.NoError(t, err)
require.NoError(t, err)
}
var listAllAfterExpiry []v1.Node
@ -1467,7 +1469,7 @@ func TestNodeRenameCommand(t *testing.T) {
}
nodes := make([]*v1.Node, len(regIDs))
assert.NoError(t, err)
require.NoError(t, err)
for index, regID := range regIDs {
_, err := headscale.Execute(
@ -1549,7 +1551,7 @@ func TestNodeRenameCommand(t *testing.T) {
fmt.Sprintf("newnode-%d", idx+1),
},
)
assert.NoError(t, err)
require.NoError(t, err)
assert.Contains(t, res, "Node renamed")
}
@ -1590,7 +1592,7 @@ func TestNodeRenameCommand(t *testing.T) {
strings.Repeat("t", 64),
},
)
assert.ErrorContains(t, err, "must not exceed 63 characters")
require.ErrorContains(t, err, "must not exceed 63 characters")
var listAllAfterRenameAttempt []v1.Node
@ -1658,7 +1660,7 @@ func TestPolicyCommand(t *testing.T) {
},
}
pBytes, _ := json.Marshal(p)
pBytes, _ := json.Marshal(p) //nolint:errchkjson
policyFilePath := "/etc/headscale/policy.json"
@ -1745,7 +1747,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
},
}
pBytes, _ := json.Marshal(p)
pBytes, _ := json.Marshal(p) //nolint:errchkjson
policyFilePath := "/etc/headscale/policy.json"
@ -1763,7 +1765,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) {
policyFilePath,
},
)
assert.ErrorContains(t, err, `invalid action "unknown-action"`)
require.ErrorContains(t, err, `invalid ACL action: "unknown-action"`)
// The new policy was invalid, the old one should still be in place, which
// is none.

View file

@ -15,8 +15,8 @@ import (
type ControlServer interface {
Shutdown() (string, string, error)
SaveLog(string) (string, string, error)
SaveProfile(string) error
SaveLog(path string) (string, string, error)
SaveProfile(path string) error
Execute(command []string) (string, error)
WriteFile(path string, content []byte) error
ConnectToNetwork(network *dockertest.Network) error
@ -35,12 +35,12 @@ type ControlServer interface {
ListUsers() ([]*v1.User, error)
MapUsers() (map[string]*v1.User, error)
DeleteUser(userID uint64) error
ApproveRoutes(uint64, []netip.Prefix) (*v1.Node, error)
ApproveRoutes(nodeID uint64, routes []netip.Prefix) (*v1.Node, error)
SetNodeTags(nodeID uint64, tags []string) error
GetCert() []byte
GetHostname() string
GetIPInNetwork(network *dockertest.Network) string
SetPolicy(*policyv2.Policy) error
SetPolicy(pol *policyv2.Policy) error
GetAllMapReponses() (map[types.NodeID][]tailcfg.MapResponse, error)
PrimaryRoutes() (*routes.DebugRoutes, error)
DebugBatcher() (*hscontrol.DebugBatcherInfo, error)

View file

@ -25,6 +25,7 @@ func TestDERPVerifyEndpoint(t *testing.T) {
// Generate random hostname for the headscale instance
hash, err := util.GenerateRandomStringDNSSafe(6)
require.NoError(t, err)
testName := "derpverify"
hostname := fmt.Sprintf("hs-%s-%s", testName, hash)
@ -40,6 +41,7 @@ func TestDERPVerifyEndpoint(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -104,13 +106,16 @@ func DERPVerify(
defer c.Close()
var result error
if err := c.Connect(t.Context()); err != nil {
err := c.Connect(t.Context())
if err != nil {
result = fmt.Errorf("client Connect: %w", err)
}
if m, err := c.Recv(); err != nil {
if m, err := c.Recv(); err != nil { //nolint:noinlineerr
result = fmt.Errorf("client first Recv: %w", err)
} else if v, ok := m.(derp.ServerInfoMessage); !ok {
result = fmt.Errorf("client first Recv was unexpected type %T", v)
result = fmt.Errorf("client first Recv was unexpected type %T", v) //nolint:err113
}
if expectSuccess && result != nil {

View file

@ -86,14 +86,13 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
const erPath = "/tmp/extra_records.json"
extraRecords := []tailcfg.DNSRecord{
{
Name: "test.myvpn.example.com",
Type: "A",
Value: "6.6.6.6",
},
}
b, _ := json.Marshal(extraRecords)
extraRecords := make([]tailcfg.DNSRecord, 0, 2)
extraRecords = append(extraRecords, tailcfg.DNSRecord{
Name: "test.myvpn.example.com",
Type: "A",
Value: "6.6.6.6",
})
b, _ := json.Marshal(extraRecords) //nolint:errchkjson
err = scenario.CreateHeadscaleEnv([]tsic.Option{
tsic.WithPackages("python3", "curl", "bind-tools"),
@ -133,7 +132,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
require.NoError(t, err)
// Write the file directly into place from the docker API.
b0, _ := json.Marshal([]tailcfg.DNSRecord{
b0, _ := json.Marshal([]tailcfg.DNSRecord{ //nolint:errchkjson
{
Name: "docker.myvpn.example.com",
Type: "A",
@ -155,7 +154,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
Type: "A",
Value: "7.7.7.7",
})
b2, _ := json.Marshal(extraRecords)
b2, _ := json.Marshal(extraRecords) //nolint:errchkjson
err = hs.WriteFile(erPath+"2", b2)
require.NoError(t, err)
@ -169,7 +168,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
// Write a new file and copy it to the path to ensure the reload
// works when a file is copied into place.
b3, _ := json.Marshal([]tailcfg.DNSRecord{
b3, _ := json.Marshal([]tailcfg.DNSRecord{ //nolint:errchkjson
{
Name: "copy.myvpn.example.com",
Type: "A",
@ -187,7 +186,7 @@ func TestResolveMagicDNSExtraRecordsPath(t *testing.T) {
}
// Write in place to ensure pipe like behaviour works
b4, _ := json.Marshal([]tailcfg.DNSRecord{
b4, _ := json.Marshal([]tailcfg.DNSRecord{ //nolint:errchkjson
{
Name: "docker.myvpn.example.com",
Type: "A",

View file

@ -34,6 +34,7 @@ func DockerAddIntegrationLabels(opts *dockertest.RunOptions, testType string) {
if opts.Labels == nil {
opts.Labels = make(map[string]string)
}
opts.Labels["hi.run-id"] = runID
opts.Labels["hi.test-type"] = testType
}

View file

@ -38,9 +38,10 @@ type buffer struct {
// Write appends the contents of p to the buffer, growing the buffer as needed. It returns
// the number of bytes written.
func (b *buffer) Write(p []byte) (n int, err error) {
func (b *buffer) Write(p []byte) (int, error) {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.store.Write(p)
}
@ -49,6 +50,7 @@ func (b *buffer) Write(p []byte) (n int, err error) {
func (b *buffer) String() string {
b.mutex.Lock()
defer b.mutex.Unlock()
return b.store.String()
}
@ -66,7 +68,8 @@ func ExecuteCommand(
}
for _, opt := range options {
if err := opt(&execConfig); err != nil {
err := opt(&execConfig)
if err != nil {
return "", "", fmt.Errorf("execute-command/options: %w", err)
}
}
@ -105,7 +108,6 @@ func ExecuteCommand(
// log.Println("Command: ", cmd)
// log.Println("stdout: ", stdout.String())
// log.Println("stderr: ", stderr.String())
return stdout.String(), stderr.String(), fmt.Errorf("command failed, stderr: %s: %w", stderr.String(), ErrDockertestCommandFailed)
}

View file

@ -47,6 +47,7 @@ func SaveLog(
}
var stdout, stderr bytes.Buffer
err = WriteLog(pool, resource, &stdout, &stderr)
if err != nil {
return "", "", err
@ -55,6 +56,7 @@ func SaveLog(
log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath)
stdoutPath := path.Join(basePath, resource.Container.Name+".stdout.log")
err = os.WriteFile(
stdoutPath,
stdout.Bytes(),
@ -65,6 +67,7 @@ func SaveLog(
}
stderrPath := path.Join(basePath, resource.Container.Name+".stderr.log")
err = os.WriteFile(
stderrPath,
stderr.Bytes(),

View file

@ -18,8 +18,9 @@ func GetFirstOrCreateNetwork(pool *dockertest.Pool, name string) (*dockertest.Ne
if err != nil {
return nil, fmt.Errorf("looking up network names: %w", err)
}
if len(networks) == 0 {
if _, err := pool.CreateNetwork(name); err == nil {
if _, err := pool.CreateNetwork(name); err == nil { //nolint:noinlineerr // intentional inline check
// Create does not give us an updated version of the resource, so we need to
// get it again.
networks, err := pool.NetworksByName(name)
@ -90,6 +91,7 @@ func RandomFreeHostPort() (int, error) {
// CleanUnreferencedNetworks removes networks that are not referenced by any containers.
func CleanUnreferencedNetworks(pool *dockertest.Pool) error {
filter := "name=hs-"
networks, err := pool.NetworksByName(filter)
if err != nil {
return fmt.Errorf("getting networks by filter %q: %w", filter, err)
@ -122,6 +124,7 @@ func CleanImagesInCI(pool *dockertest.Pool) error {
}
removedCount := 0
for _, image := range images {
// Only remove dangling (untagged) images to avoid forcing rebuilds
// Dangling images have no RepoTags or only have "<none>:<none>"

View file

@ -159,10 +159,12 @@ func New(
} else {
hostname = fmt.Sprintf("derp-%s-%s", strings.ReplaceAll(version, ".", "-"), hash)
}
tlsCert, tlsKey, err := integrationutil.CreateCertificate(hostname)
if err != nil {
return nil, fmt.Errorf("creating certificates for headscale test: %w", err)
}
dsic := &DERPServerInContainer{
version: version,
hostname: hostname,
@ -185,6 +187,7 @@ func New(
fmt.Fprintf(&cmdArgs, " --a=:%d", dsic.derpPort)
fmt.Fprintf(&cmdArgs, " --stun=true")
fmt.Fprintf(&cmdArgs, " --stun-port=%d", dsic.stunPort)
if dsic.withVerifyClientURL != "" {
fmt.Fprintf(&cmdArgs, " --verify-client-url=%s", dsic.withVerifyClientURL)
}
@ -214,11 +217,13 @@ func New(
}
var container *dockertest.Resource
buildOptions := &dockertest.BuildOptions{
Dockerfile: "Dockerfile.derper",
ContextDir: dockerContextPath,
BuildArgs: []docker.BuildArg{},
}
switch version {
case "head":
buildOptions.BuildArgs = append(buildOptions.BuildArgs, docker.BuildArg{
@ -249,6 +254,7 @@ func New(
err,
)
}
log.Printf("Created %s container\n", hostname)
dsic.container = container
@ -259,12 +265,14 @@ func New(
return nil, fmt.Errorf("writing TLS certificate to container: %w", err)
}
}
if len(dsic.tlsCert) != 0 {
err = dsic.WriteFile(fmt.Sprintf("%s/%s.crt", DERPerCertRoot, dsic.hostname), dsic.tlsCert)
if err != nil {
return nil, fmt.Errorf("writing TLS certificate to container: %w", err)
}
}
if len(dsic.tlsKey) != 0 {
err = dsic.WriteFile(fmt.Sprintf("%s/%s.key", DERPerCertRoot, dsic.hostname), dsic.tlsKey)
if err != nil {

View file

@ -3,9 +3,12 @@ package integration
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"maps"
"net/netip"
"slices"
"strconv"
"strings"
"sync"
@ -23,8 +26,6 @@ import (
"github.com/oauth2-proxy/mockoidc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
@ -46,9 +47,16 @@ const (
// TimestampFormatRunID is used for generating unique run identifiers
// Format: "20060102-150405" provides compact date-time for file/directory names.
TimestampFormatRunID = "20060102-150405"
// stateOnline is the string representation for online state in logs.
stateOnline = "online"
// stateOffline is the string representation for offline state in logs.
stateOffline = "offline"
)
// NodeSystemStatus represents the status of a node across different systems
var errNoNewClientFound = errors.New("no new client found")
// NodeSystemStatus represents the status of a node across different systems.
type NodeSystemStatus struct {
Batcher bool
BatcherConnCount int
@ -105,7 +113,7 @@ func requireNoErrLogout(t *testing.T, err error) {
require.NoError(t, err, "failed to log out tailscale nodes")
}
// collectExpectedNodeIDs extracts node IDs from a list of TailscaleClients for validation purposes
// collectExpectedNodeIDs extracts node IDs from a list of TailscaleClients for validation purposes.
func collectExpectedNodeIDs(t *testing.T, clients []TailscaleClient) []types.NodeID {
t.Helper()
@ -114,8 +122,10 @@ func collectExpectedNodeIDs(t *testing.T, clients []TailscaleClient) []types.Nod
status := client.MustStatus()
nodeID, err := strconv.ParseUint(string(status.Self.ID), 10, 64)
require.NoError(t, err)
expectedNodes = append(expectedNodes, types.NodeID(nodeID))
}
return expectedNodes
}
@ -149,15 +159,17 @@ func validateReloginComplete(t *testing.T, headscale ControlServer, expectedNode
}
// requireAllClientsOnline validates that all nodes are online/offline across all headscale systems
// requireAllClientsOnline verifies all expected nodes are in the specified online state across all systems
// requireAllClientsOnline verifies all expected nodes are in the specified online state across all systems.
func requireAllClientsOnline(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, expectedOnline bool, message string, timeout time.Duration) {
t.Helper()
startTime := time.Now()
stateStr := "offline"
stateStr := stateOffline
if expectedOnline {
stateStr = "online"
stateStr = stateOnline
}
t.Logf("requireAllSystemsOnline: Starting %s validation for %d nodes at %s - %s", stateStr, len(expectedNodes), startTime.Format(TimestampFormat), message)
if expectedOnline {
@ -165,22 +177,26 @@ func requireAllClientsOnline(t *testing.T, headscale ControlServer, expectedNode
requireAllClientsOnlineWithSingleTimeout(t, headscale, expectedNodes, expectedOnline, message, timeout)
} else {
// For offline validation, use staged approach with component-specific timeouts
requireAllClientsOfflineStaged(t, headscale, expectedNodes, message, timeout)
requireAllClientsOfflineStaged(t, headscale, expectedNodes)
}
endTime := time.Now()
t.Logf("requireAllSystemsOnline: Completed %s validation for %d nodes at %s - Duration: %s - %s", stateStr, len(expectedNodes), endTime.Format(TimestampFormat), endTime.Sub(startTime), message)
}
// requireAllClientsOnlineWithSingleTimeout is the original validation logic for online state
// requireAllClientsOnlineWithSingleTimeout is the original validation logic for online state.
//
//nolint:gocyclo // complex validation with multiple node states
func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, expectedOnline bool, message string, timeout time.Duration) {
t.Helper()
var prevReport string
require.EventuallyWithT(t, func(c *assert.CollectT) {
// Get batcher state
debugInfo, err := headscale.DebugBatcher()
assert.NoError(c, err, "Failed to get batcher debug info")
if err != nil {
return
}
@ -188,6 +204,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
// Get map responses
mapResponses, err := headscale.GetAllMapReponses()
assert.NoError(c, err, "Failed to get map responses")
if err != nil {
return
}
@ -195,6 +212,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
// Get nodestore state
nodeStore, err := headscale.DebugNodeStore()
assert.NoError(c, err, "Failed to get nodestore debug info")
if err != nil {
return
}
@ -265,6 +283,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
if id == nodeID {
continue // Skip self-references
}
expectedPeerMaps++
if online, exists := peerMap[nodeID]; exists && online {
@ -279,6 +298,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
}
}
}
assert.Lenf(c, onlineFromMaps, expectedCount, "MapResponses missing nodes in status check")
// Update status with map response data
@ -302,10 +322,12 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
// Verify all systems show nodes in expected state and report failures
allMatch := true
var failureReport strings.Builder
ids := types.NodeIDs(maps.Keys(nodeStatus))
ids := types.NodeIDs(slices.AppendSeq(make([]types.NodeID, 0, len(nodeStatus)), maps.Keys(nodeStatus)))
slices.Sort(ids)
for _, nodeID := range ids {
status := nodeStatus[nodeID]
systemsMatch := (status.Batcher == expectedOnline) &&
@ -314,10 +336,12 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
if !systemsMatch {
allMatch = false
stateStr := "offline"
stateStr := stateOffline
if expectedOnline {
stateStr = "online"
stateStr = stateOnline
}
failureReport.WriteString(fmt.Sprintf("node:%d is not fully %s (timestamp: %s):\n", nodeID, stateStr, time.Now().Format(TimestampFormat)))
failureReport.WriteString(fmt.Sprintf(" - batcher: %t (expected: %t)\n", status.Batcher, expectedOnline))
failureReport.WriteString(fmt.Sprintf(" - conn count: %d\n", status.BatcherConnCount))
@ -332,6 +356,7 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
t.Logf("Previous report:\n%s", prevReport)
t.Logf("Current report:\n%s", failureReport.String())
t.Logf("Report diff:\n%s", diff)
prevReport = failureReport.String()
}
@ -341,16 +366,17 @@ func requireAllClientsOnlineWithSingleTimeout(t *testing.T, headscale ControlSer
assert.Fail(c, failureReport.String())
}
stateStr := "offline"
stateStr := stateOffline
if expectedOnline {
stateStr = "online"
stateStr = stateOnline
}
assert.True(c, allMatch, fmt.Sprintf("Not all %d nodes are %s across all systems (batcher, mapresponses, nodestore)", len(expectedNodes), stateStr))
assert.True(c, allMatch, "Not all %d nodes are %s across all systems (batcher, mapresponses, nodestore)", len(expectedNodes), stateStr)
}, timeout, 2*time.Second, message)
}
// requireAllClientsOfflineStaged validates offline state with staged timeouts for different components
func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, message string, totalTimeout time.Duration) {
// requireAllClientsOfflineStaged validates offline state with staged timeouts for different components.
func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID) {
t.Helper()
// Stage 1: Verify batcher disconnection (should be immediate)
@ -358,18 +384,22 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
require.EventuallyWithT(t, func(c *assert.CollectT) {
debugInfo, err := headscale.DebugBatcher()
assert.NoError(c, err, "Failed to get batcher debug info")
if err != nil {
return
}
allBatcherOffline := true
for _, nodeID := range expectedNodes {
nodeIDStr := fmt.Sprintf("%d", nodeID)
if nodeInfo, exists := debugInfo.ConnectedNodes[nodeIDStr]; exists && nodeInfo.Connected {
allBatcherOffline = false
assert.False(c, nodeInfo.Connected, "Node %d should not be connected in batcher", nodeID)
}
}
assert.True(c, allBatcherOffline, "All nodes should be disconnected from batcher")
}, 15*time.Second, 1*time.Second, "batcher disconnection validation")
@ -378,20 +408,24 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
require.EventuallyWithT(t, func(c *assert.CollectT) {
nodeStore, err := headscale.DebugNodeStore()
assert.NoError(c, err, "Failed to get nodestore debug info")
if err != nil {
return
}
allNodeStoreOffline := true
for _, nodeID := range expectedNodes {
if node, exists := nodeStore[nodeID]; exists {
isOnline := node.IsOnline != nil && *node.IsOnline
if isOnline {
allNodeStoreOffline = false
assert.False(c, isOnline, "Node %d should be offline in nodestore", nodeID)
}
}
}
assert.True(c, allNodeStoreOffline, "All nodes should be offline in nodestore")
}, 20*time.Second, 1*time.Second, "nodestore offline validation")
@ -400,6 +434,7 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
require.EventuallyWithT(t, func(c *assert.CollectT) {
mapResponses, err := headscale.GetAllMapReponses()
assert.NoError(c, err, "Failed to get map responses")
if err != nil {
return
}
@ -412,7 +447,8 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
for nodeID := range onlineMap {
if slices.Contains(expectedNodes, nodeID) {
allMapResponsesOffline = false
assert.False(c, true, "Node %d should not appear in map responses", nodeID)
assert.Fail(c, fmt.Sprintf("Node %d should not appear in map responses", nodeID))
}
}
} else {
@ -422,13 +458,16 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
if id == nodeID {
continue // Skip self-references
}
if online, exists := peerMap[nodeID]; exists && online {
allMapResponsesOffline = false
assert.False(c, online, "Node %d should not be visible in node %d's map response", nodeID, id)
}
}
}
}
assert.True(c, allMapResponsesOffline, "All nodes should be absent from peer map responses")
}, 60*time.Second, 2*time.Second, "map response propagation validation")
@ -438,6 +477,8 @@ func requireAllClientsOfflineStaged(t *testing.T, headscale ControlServer, expec
// requireAllClientsNetInfoAndDERP validates that all nodes have NetInfo in the database
// and a valid DERP server based on the NetInfo. This function follows the pattern of
// requireAllClientsOnline by using hsic.DebugNodeStore to get the database state.
//
//nolint:unparam // timeout is configurable for flexibility even though callers currently use same value
func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expectedNodes []types.NodeID, message string, timeout time.Duration) {
t.Helper()
@ -448,6 +489,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
// Get nodestore state
nodeStore, err := headscale.DebugNodeStore()
assert.NoError(c, err, "Failed to get nodestore debug info")
if err != nil {
return
}
@ -462,12 +504,14 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
for _, nodeID := range expectedNodes {
node, exists := nodeStore[nodeID]
assert.True(c, exists, "Node %d not found in nodestore during NetInfo validation", nodeID)
if !exists {
continue
}
// Validate that the node has Hostinfo
assert.NotNil(c, node.Hostinfo, "Node %d (%s) should have Hostinfo for NetInfo validation", nodeID, node.Hostname)
if node.Hostinfo == nil {
t.Logf("Node %d (%s) missing Hostinfo at %s", nodeID, node.Hostname, time.Now().Format(TimestampFormat))
continue
@ -475,6 +519,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
// Validate that the node has NetInfo
assert.NotNil(c, node.Hostinfo.NetInfo, "Node %d (%s) should have NetInfo in Hostinfo for DERP connectivity", nodeID, node.Hostname)
if node.Hostinfo.NetInfo == nil {
t.Logf("Node %d (%s) missing NetInfo at %s", nodeID, node.Hostname, time.Now().Format(TimestampFormat))
continue
@ -482,7 +527,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
// Validate that the node has a valid DERP server (PreferredDERP should be > 0)
preferredDERP := node.Hostinfo.NetInfo.PreferredDERP
assert.Greater(c, preferredDERP, 0, "Node %d (%s) should have a valid DERP server (PreferredDERP > 0) for relay connectivity, got %d", nodeID, node.Hostname, preferredDERP)
assert.Positive(c, preferredDERP, "Node %d (%s) should have a valid DERP server (PreferredDERP > 0) for relay connectivity, got %d", nodeID, node.Hostname, preferredDERP)
t.Logf("Node %d (%s) has valid NetInfo with DERP server %d at %s", nodeID, node.Hostname, preferredDERP, time.Now().Format(TimestampFormat))
}
@ -496,6 +541,7 @@ func requireAllClientsNetInfoAndDERP(t *testing.T, headscale ControlServer, expe
// assertLastSeenSet validates that a node has a non-nil LastSeen timestamp.
// Critical for ensuring node activity tracking is functioning properly.
func assertLastSeenSet(t *testing.T, node *v1.Node) {
t.Helper()
assert.NotNil(t, node)
assert.NotNil(t, node.GetLastSeen())
}
@ -514,7 +560,7 @@ func assertTailscaleNodesLogout(t assert.TestingT, clients []TailscaleClient) {
for _, client := range clients {
status, err := client.Status()
assert.NoError(t, err, "failed to get status for client %s", client.Hostname())
assert.NoError(t, err, "failed to get status for client %s", client.Hostname()) //nolint:testifylint // assert.TestingT interface
assert.Equal(t, "NeedsLogin", status.BackendState,
"client %s should be logged out", client.Hostname())
}
@ -523,8 +569,11 @@ func assertTailscaleNodesLogout(t assert.TestingT, clients []TailscaleClient) {
// pingAllHelper performs ping tests between all clients and addresses, returning success count.
// This is used to validate network connectivity in integration tests.
// Returns the total number of successful ping operations.
//
//nolint:unparam // opts is variadic for extensibility even though callers currently don't pass options
func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string, opts ...tsic.PingOption) int {
t.Helper()
success := 0
for _, client := range clients {
@ -546,6 +595,7 @@ func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string, opts
// for validating NAT traversal and relay functionality. Returns success count.
func pingDerpAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) int {
t.Helper()
success := 0
for _, client := range clients {
@ -596,6 +646,8 @@ func isSelfClient(client TailscaleClient, addr string) bool {
// assertClientsState validates the status and netmap of a list of clients for general connectivity.
// Runs parallel validation of status, netcheck, and netmap for all clients to ensure
// they have proper network configuration for all-to-all connectivity tests.
//
//nolint:unused
func assertClientsState(t *testing.T, clients []TailscaleClient) {
t.Helper()
@ -603,9 +655,12 @@ func assertClientsState(t *testing.T, clients []TailscaleClient) {
for _, client := range clients {
wg.Add(1)
c := client // Avoid loop pointer
go func() {
defer wg.Done()
assertValidStatus(t, c)
assertValidNetcheck(t, c)
assertValidNetmap(t, c)
@ -620,6 +675,8 @@ func assertClientsState(t *testing.T, clients []TailscaleClient) {
// Checks self node and all peers for essential networking data including hostinfo, addresses,
// endpoints, and DERP configuration. Skips validation for Tailscale versions below 1.56.
// This test is not suitable for ACL/partial connection tests.
//
//nolint:unused
func assertValidNetmap(t *testing.T, client TailscaleClient) {
t.Helper()
@ -636,6 +693,7 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
assert.NoError(c, err, "getting netmap for %q", client.Hostname())
assert.Truef(c, netmap.SelfNode.Hostinfo().Valid(), "%q does not have Hostinfo", client.Hostname())
if hi := netmap.SelfNode.Hostinfo(); hi.Valid() {
assert.LessOrEqual(c, 1, netmap.SelfNode.Hostinfo().Services().Len(), "%q does not have enough services, got: %v", client.Hostname(), netmap.SelfNode.Hostinfo().Services())
}
@ -650,10 +708,11 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
assert.Falsef(c, netmap.SelfNode.DiscoKey().IsZero(), "%q does not have a valid DiscoKey", client.Hostname())
for _, peer := range netmap.Peers {
assert.NotEqualf(c, "127.3.3.40:0", peer.LegacyDERPString(), "peer (%s) has no home DERP in %q's netmap, got: %s", peer.ComputedName(), client.Hostname(), peer.LegacyDERPString())
assert.NotEqualf(c, "127.3.3.40:0", peer.LegacyDERPString(), "peer (%s) has no home DERP in %q's netmap, got: %s", peer.ComputedName(), client.Hostname(), peer.LegacyDERPString()) //nolint:staticcheck // SA1019: testing legacy field
assert.NotEqualf(c, 0, peer.HomeDERP(), "peer (%s) has no home DERP in %q's netmap, got: %d", peer.ComputedName(), client.Hostname(), peer.HomeDERP())
assert.Truef(c, peer.Hostinfo().Valid(), "peer (%s) of %q does not have Hostinfo", peer.ComputedName(), client.Hostname())
if hi := peer.Hostinfo(); hi.Valid() {
assert.LessOrEqualf(c, 3, peer.Hostinfo().Services().Len(), "peer (%s) of %q does not have enough services, got: %v", peer.ComputedName(), client.Hostname(), peer.Hostinfo().Services())
@ -680,8 +739,11 @@ func assertValidNetmap(t *testing.T, client TailscaleClient) {
// assertValidStatus validates that a client's status has all required fields for proper operation.
// Checks self and peer status for essential data including hostinfo, tailscale IPs, endpoints,
// and network map presence. This test is not suitable for ACL/partial connection tests.
//
//nolint:unused
func assertValidStatus(t *testing.T, client TailscaleClient) {
t.Helper()
status, err := client.Status(true)
if err != nil {
t.Fatalf("getting status for %q: %s", client.Hostname(), err)
@ -737,8 +799,11 @@ func assertValidStatus(t *testing.T, client TailscaleClient) {
// assertValidNetcheck validates that a client has a proper DERP relay configured.
// Ensures the client has discovered and selected a DERP server for relay functionality,
// which is essential for NAT traversal and connectivity in restricted networks.
//
//nolint:unused
func assertValidNetcheck(t *testing.T, client TailscaleClient) {
t.Helper()
report, err := client.Netcheck()
if err != nil {
t.Fatalf("getting status for %q: %s", client.Hostname(), err)
@ -764,7 +829,7 @@ func assertCommandOutputContains(t *testing.T, c TailscaleClient, command []stri
}
if !strings.Contains(stdout, contains) {
return struct{}{}, fmt.Errorf("executing command, expected string %q not found in %q", contains, stdout)
return struct{}{}, fmt.Errorf("executing command, expected string %q not found in %q", contains, stdout) //nolint:err113
}
return struct{}{}, nil
@ -793,6 +858,7 @@ func didClientUseWebsocketForDERP(t *testing.T, client TailscaleClient) bool {
t.Helper()
buf := &bytes.Buffer{}
err := client.WriteLogs(buf, buf)
if err != nil {
t.Fatalf("failed to fetch client logs: %s: %s", client.Hostname(), err)
@ -816,6 +882,7 @@ func countMatchingLines(in io.Reader, predicate func(string) bool) (int, error)
scanner := bufio.NewScanner(in)
{
const logBufferInitialSize = 1024 << 10 // preallocate 1 MiB
buff := make([]byte, logBufferInitialSize)
scanner.Buffer(buff, len(buff))
scanner.Split(bufio.ScanLines)
@ -885,6 +952,8 @@ func usernameOwner(name string) policyv2.Owner {
// groupOwner returns a Group as an Owner for use in TagOwners policies.
// Specifies which groups can assign and manage specific tags in ACL configurations.
//
//nolint:unused
func groupOwner(name string) policyv2.Owner {
return ptr.To(policyv2.Group(name))
}
@ -933,7 +1002,7 @@ func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
}
}
return nil, fmt.Errorf("user %s not found", username)
return nil, fmt.Errorf("user %s not found", username) //nolint:err113
}
// FindNewClient finds a client that is in the new list but not in the original list.
@ -942,17 +1011,20 @@ func GetUserByName(headscale ControlServer, username string) (*v1.User, error) {
func FindNewClient(original, updated []TailscaleClient) (TailscaleClient, error) {
for _, client := range updated {
isOriginal := false
for _, origClient := range original {
if client.Hostname() == origClient.Hostname() {
isOriginal = true
break
}
}
if !isOriginal {
return client, nil
}
}
return nil, fmt.Errorf("no new client found")
return nil, errNoNewClientFound
}
// AddAndLoginClient adds a new tailscale client to a user and logs it in.
@ -960,7 +1032,7 @@ func FindNewClient(original, updated []TailscaleClient) (TailscaleClient, error)
// 1. Creating a new node
// 2. Finding the new node in the client list
// 3. Getting the user to create a preauth key
// 4. Logging in the new node
// 4. Logging in the new node.
func (s *Scenario) AddAndLoginClient(
t *testing.T,
username string,
@ -992,7 +1064,7 @@ func (s *Scenario) AddAndLoginClient(
}
if len(updatedClients) != len(originalClients)+1 {
return struct{}{}, fmt.Errorf("expected %d clients, got %d", len(originalClients)+1, len(updatedClients))
return struct{}{}, fmt.Errorf("expected %d clients, got %d", len(originalClients)+1, len(updatedClients)) //nolint:err113
}
newClient, err = FindNewClient(originalClients, updatedClients)
@ -1038,5 +1110,6 @@ func (s *Scenario) MustAddAndLoginClient(
client, err := s.AddAndLoginClient(t, username, version, headscale, tsOpts...)
require.NoError(t, err)
return client
}

View file

@ -4,6 +4,7 @@ import (
"archive/tar"
"bytes"
"cmp"
"context"
"crypto/tls"
"encoding/json"
"errors"
@ -11,6 +12,7 @@ import (
"io"
"log"
"maps"
"net"
"net/http"
"net/netip"
"os"
@ -46,6 +48,7 @@ const (
tlsKeyPath = "/etc/headscale/tls.key"
headscaleDefaultPort = 8080
IntegrationTestDockerFileName = "Dockerfile.integration"
defaultDirPerm = 0o755
)
var (
@ -198,7 +201,7 @@ func WithPostgres() Option {
}
}
// WithPolicy sets the policy mode for headscale.
// WithPolicyMode sets the policy mode for headscale.
func WithPolicyMode(mode types.PolicyMode) Option {
return func(hsic *HeadscaleInContainer) {
hsic.policyMode = mode
@ -217,6 +220,8 @@ func WithIPAllocationStrategy(strategy types.IPAllocationStrategy) Option {
// and only use the embedded DERP server.
// It requires WithTLS and WithHostnameAsServerURL to be
// set.
//
//nolint:goconst // env var values like "true" and "headscale" are clearer inline
func WithEmbeddedDERPServerOnly() Option {
return func(hsic *HeadscaleInContainer) {
hsic.env["HEADSCALE_DERP_URLS"] = ""
@ -321,6 +326,8 @@ func (hsic *HeadscaleInContainer) buildEntrypoint() []string {
}
// New returns a new HeadscaleInContainer instance.
//
//nolint:gocyclo // complex container setup with many options
func New(
pool *dockertest.Pool,
networks []*dockertest.Network,
@ -548,6 +555,7 @@ func New(
return nil, fmt.Errorf("starting headscale container: %w\n\nUnable to get diagnostic build output (command may have failed silently)", err)
}
}
log.Printf("Created %s container\n", hsic.hostname)
hsic.container = container
@ -595,7 +603,8 @@ func New(
}
for _, f := range hsic.filesInContainer {
if err := hsic.WriteFile(f.path, f.contents); err != nil {
err := hsic.WriteFile(f.path, f.contents)
if err != nil {
return nil, fmt.Errorf("writing %q: %w", f.path, err)
}
}
@ -678,7 +687,7 @@ func (t *HeadscaleInContainer) Shutdown() (string, string, error) {
// Cleanup postgres container if enabled.
if t.postgres {
t.pool.Purge(t.pgContainer)
_ = t.pool.Purge(t.pgContainer)
}
return stdoutPath, stderrPath, t.pool.Purge(t.container)
@ -697,16 +706,23 @@ func (t *HeadscaleInContainer) SaveLog(path string) (string, string, error) {
}
func (t *HeadscaleInContainer) SaveMetrics(savePath string) error {
resp, err := http.Get(fmt.Sprintf("http://%s:9090/metrics", t.hostname))
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://"+net.JoinHostPort(t.hostname, "9090")+"/metrics", nil)
if err != nil {
return fmt.Errorf("creating metrics request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("getting metrics: %w", err)
}
defer resp.Body.Close()
out, err := os.Create(savePath)
if err != nil {
return fmt.Errorf("creating file for metrics: %w", err)
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("copy response to file: %w", err)
@ -717,20 +733,21 @@ func (t *HeadscaleInContainer) SaveMetrics(savePath string) error {
// extractTarToDirectory extracts a tar archive to a directory.
func extractTarToDirectory(tarData []byte, targetDir string) error {
if err := os.MkdirAll(targetDir, 0o755); err != nil {
err := os.MkdirAll(targetDir, defaultDirPerm)
if err != nil {
return fmt.Errorf("creating directory %s: %w", targetDir, err)
}
tarReader := tar.NewReader(bytes.NewReader(tarData))
// Find the top-level directory to strip
var topLevelDir string
firstPass := tar.NewReader(bytes.NewReader(tarData))
for {
header, err := firstPass.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading tar header: %w", err)
}
@ -741,12 +758,13 @@ func extractTarToDirectory(tarData []byte, targetDir string) error {
}
}
tarReader = tar.NewReader(bytes.NewReader(tarData))
tarReader := tar.NewReader(bytes.NewReader(tarData))
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading tar header: %w", err)
}
@ -775,12 +793,15 @@ func extractTarToDirectory(tarData []byte, targetDir string) error {
switch header.Typeflag {
case tar.TypeDir:
// Create directory
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
//nolint:gosec // G115: header.Mode is trusted from tar archive
err := os.MkdirAll(targetPath, os.FileMode(header.Mode))
if err != nil {
return fmt.Errorf("creating directory %s: %w", targetPath, err)
}
case tar.TypeReg:
// Ensure parent directories exist
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
err := os.MkdirAll(filepath.Dir(targetPath), defaultDirPerm)
if err != nil {
return fmt.Errorf("creating parent directories for %s: %w", targetPath, err)
}
@ -790,14 +811,15 @@ func extractTarToDirectory(tarData []byte, targetDir string) error {
return fmt.Errorf("creating file %s: %w", targetPath, err)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
if _, err := io.Copy(outFile, tarReader); err != nil { //nolint:gosec,noinlineerr // trusted tar from test container
outFile.Close()
return fmt.Errorf("copying file contents: %w", err)
}
outFile.Close()
// Set file permissions
if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil {
if err := os.Chmod(targetPath, os.FileMode(header.Mode)); err != nil { //nolint:gosec,noinlineerr // safe mode from tar header
return fmt.Errorf("setting file permissions: %w", err)
}
}
@ -844,10 +866,12 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
// Check if the database file exists and has a schema
dbPath := "/tmp/integration_test_db.sqlite3"
fileInfo, err := t.Execute([]string{"ls", "-la", dbPath})
if err != nil {
return fmt.Errorf("database file does not exist at %s: %w", dbPath, err)
}
log.Printf("Database file info: %s", fileInfo)
// Check if the database has any tables (schema)
@ -857,7 +881,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
}
if strings.TrimSpace(schemaCheck) == "" {
return errors.New("database file exists but has no schema (empty database)")
return errors.New("database file exists but has no schema (empty database)") //nolint:err113
}
tarFile, err := t.FetchPath("/tmp/integration_test_db.sqlite3")
@ -872,6 +896,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading tar header: %w", err)
}
@ -886,13 +911,15 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
// Extract the first regular file we find
if header.Typeflag == tar.TypeReg {
dbPath := path.Join(savePath, t.hostname+".db")
outFile, err := os.Create(dbPath)
if err != nil {
return fmt.Errorf("creating database file: %w", err)
}
written, err := io.Copy(outFile, tarReader)
written, err := io.Copy(outFile, tarReader) //nolint:gosec // trusted tar from test container
outFile.Close()
if err != nil {
return fmt.Errorf("copying database file: %w", err)
}
@ -906,7 +933,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
// Check if we actually wrote something
if written == 0 {
return fmt.Errorf(
return fmt.Errorf( //nolint:err113
"database file is empty (size: %d, header size: %d)",
written,
header.Size,
@ -917,7 +944,7 @@ func (t *HeadscaleInContainer) SaveDatabase(savePath string) error {
}
}
return errors.New("no regular file found in database tar archive")
return errors.New("no regular file found in database tar archive") //nolint:err113
}
// Execute runs a command inside the Headscale container and returns the
@ -1059,6 +1086,7 @@ func (t *HeadscaleInContainer) CreateUser(
}
var u v1.User
err = json.Unmarshal([]byte(result), &u)
if err != nil {
return nil, fmt.Errorf("unmarshalling user: %w", err)
@ -1195,6 +1223,7 @@ func (t *HeadscaleInContainer) ListNodes(
users ...string,
) ([]*v1.Node, error) {
var ret []*v1.Node
execUnmarshal := func(command []string) error {
result, _, err := dockertestutil.ExecuteCommand(
t.container,
@ -1206,6 +1235,7 @@ func (t *HeadscaleInContainer) ListNodes(
}
var nodes []*v1.Node
err = json.Unmarshal([]byte(result), &nodes)
if err != nil {
return fmt.Errorf("unmarshalling nodes: %w", err)
@ -1245,7 +1275,7 @@ func (t *HeadscaleInContainer) DeleteNode(nodeID uint64) error {
"nodes",
"delete",
"--identifier",
fmt.Sprintf("%d", nodeID),
strconv.FormatUint(nodeID, 10),
"--output",
"json",
"--force",
@ -1309,6 +1339,7 @@ func (t *HeadscaleInContainer) ListUsers() ([]*v1.User, error) {
}
var users []*v1.User
err = json.Unmarshal([]byte(result), &users)
if err != nil {
return nil, fmt.Errorf("unmarshalling nodes: %w", err)
@ -1439,6 +1470,7 @@ func (h *HeadscaleInContainer) PID() (int, error) {
if pidInt == 1 {
continue
}
pids = append(pids, pidInt)
}
@ -1494,6 +1526,7 @@ func (t *HeadscaleInContainer) ApproveRoutes(id uint64, routes []netip.Prefix) (
}
var node *v1.Node
err = json.Unmarshal([]byte(result), &node)
if err != nil {
return nil, fmt.Errorf("unmarshalling node response: %q, error: %w", result, err)
@ -1569,7 +1602,7 @@ func (t *HeadscaleInContainer) GetAllMapReponses() (map[types.NodeID][]tailcfg.M
}
var res map[types.NodeID][]tailcfg.MapResponse
if err := json.Unmarshal([]byte(result), &res); err != nil {
if err := json.Unmarshal([]byte(result), &res); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("decoding routes response: %w", err)
}
@ -1589,7 +1622,7 @@ func (t *HeadscaleInContainer) PrimaryRoutes() (*routes.DebugRoutes, error) {
}
var debugRoutes routes.DebugRoutes
if err := json.Unmarshal([]byte(result), &debugRoutes); err != nil {
if err := json.Unmarshal([]byte(result), &debugRoutes); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("decoding routes response: %w", err)
}
@ -1609,7 +1642,7 @@ func (t *HeadscaleInContainer) DebugBatcher() (*hscontrol.DebugBatcherInfo, erro
}
var debugInfo hscontrol.DebugBatcherInfo
if err := json.Unmarshal([]byte(result), &debugInfo); err != nil {
if err := json.Unmarshal([]byte(result), &debugInfo); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("decoding batcher debug response: %w", err)
}
@ -1629,7 +1662,7 @@ func (t *HeadscaleInContainer) DebugNodeStore() (map[types.NodeID]types.Node, er
}
var nodeStore map[types.NodeID]types.Node
if err := json.Unmarshal([]byte(result), &nodeStore); err != nil {
if err := json.Unmarshal([]byte(result), &nodeStore); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("decoding nodestore debug response: %w", err)
}
@ -1649,7 +1682,7 @@ func (t *HeadscaleInContainer) DebugFilter() ([]tailcfg.FilterRule, error) {
}
var filterRules []tailcfg.FilterRule
if err := json.Unmarshal([]byte(result), &filterRules); err != nil {
if err := json.Unmarshal([]byte(result), &filterRules); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("decoding filter response: %w", err)
}

View file

@ -28,6 +28,7 @@ func PeerSyncTimeout() time.Duration {
if util.IsCI() {
return 120 * time.Second
}
return 60 * time.Second
}
@ -205,25 +206,27 @@ func BuildExpectedOnlineMap(all map[types.NodeID][]tailcfg.MapResponse) map[type
res := make(map[types.NodeID]map[types.NodeID]bool)
for nid, mrs := range all {
res[nid] = make(map[types.NodeID]bool)
for _, mr := range mrs {
for _, peer := range mr.Peers {
if peer.Online != nil {
res[nid][types.NodeID(peer.ID)] = *peer.Online
res[nid][types.NodeID(peer.ID)] = *peer.Online //nolint:gosec // safe conversion for peer ID
}
}
for _, peer := range mr.PeersChanged {
if peer.Online != nil {
res[nid][types.NodeID(peer.ID)] = *peer.Online
res[nid][types.NodeID(peer.ID)] = *peer.Online //nolint:gosec // safe conversion for peer ID
}
}
for _, peer := range mr.PeersChangedPatch {
if peer.Online != nil {
res[nid][types.NodeID(peer.NodeID)] = *peer.Online
res[nid][types.NodeID(peer.NodeID)] = *peer.Online //nolint:gosec // safe conversion for peer ID
}
}
}
}
return res
}

View file

@ -49,6 +49,7 @@ func TestEnablingRoutes(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
@ -91,6 +92,7 @@ func TestEnablingRoutes(t *testing.T) {
// Wait for route advertisements to propagate to NodeStore
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(ct, err)
@ -127,6 +129,7 @@ func TestEnablingRoutes(t *testing.T) {
// Wait for route approvals to propagate to NodeStore
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(ct, err)
@ -149,9 +152,11 @@ func TestEnablingRoutes(t *testing.T) {
assert.NotNil(c, peerStatus.PrimaryRoutes)
assert.NotNil(c, peerStatus.AllowedIPs)
if peerStatus.AllowedIPs != nil {
assert.Len(c, peerStatus.AllowedIPs.AsSlice(), 3)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{netip.MustParsePrefix(expectedRoutes[string(peerStatus.ID)])})
}
}
@ -172,6 +177,7 @@ func TestEnablingRoutes(t *testing.T) {
// Wait for route state changes to propagate to nodes
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(c, err)
@ -214,6 +220,7 @@ func TestEnablingRoutes(t *testing.T) {
}
}
//nolint:gocyclo // complex HA failover test scenario
func TestHASubnetRouterFailover(t *testing.T) {
IntegrationSkip(t)
@ -271,6 +278,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
prefp, err := scenario.SubnetOfNetwork("usernet1")
require.NoError(t, err)
pref := *prefp
t.Logf("usernet1 prefix: %s", pref.String())
@ -310,6 +318,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" - Router 2 (%s): Advertising route %s - will be STANDBY when approved", subRouter2.Hostname(), pref.String())
t.Logf(" - Router 3 (%s): Advertising route %s - will be STANDBY when approved", subRouter3.Hostname(), pref.String())
t.Logf(" Expected: All 3 routers advertise the same route for redundancy, but only one will be primary at a time")
for _, client := range allClients[:3] {
command := []string{
"tailscale",
@ -325,6 +334,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
// Wait for route configuration changes after advertising routes
var nodes []*v1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
nodes, err = headscale.ListNodes()
assert.NoError(c, err)
@ -361,13 +371,15 @@ func TestHASubnetRouterFailover(t *testing.T) {
)
// Helper function to check test failure and print route map if needed
checkFailureAndPrintRoutes := func(t *testing.T, client TailscaleClient) {
checkFailureAndPrintRoutes := func(t *testing.T, client TailscaleClient) { //nolint:thelper
if t.Failed() {
t.Logf("[%s] Test failed at this checkpoint", time.Now().Format(TimestampFormat))
status, err := client.Status()
if err == nil {
printCurrentRouteMap(t, xmaps.Values(status.Peer)...)
}
t.FailNow()
}
}
@ -386,6 +398,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 1 becomes PRIMARY with route %s active", pref.String())
t.Logf(" Expected: Routers 2 & 3 remain with advertised but unapproved routes")
t.Logf(" Expected: Client can access webservice through router 1 only")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter1.Hostname(), nodes).GetId(),
[]netip.Prefix{pref},
@ -456,10 +469,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter1.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter1") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 1")
@ -483,6 +498,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 2 becomes STANDBY (approved but not primary)")
t.Logf(" Expected: Router 1 remains PRIMARY (no flapping - stability preferred)")
t.Logf(" Expected: HA is now active - if router 1 fails, router 2 can take over")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter2.Hostname(), nodes).GetId(),
[]netip.Prefix{pref},
@ -494,6 +510,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
nodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, nodes, 6)
if len(nodes) >= 3 {
requireNodeRouteCountWithCollect(c, nodes[0], 1, 1, 1)
requireNodeRouteCountWithCollect(c, nodes[1], 1, 1, 0)
@ -569,10 +586,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter1.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter1") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 1 in HA mode")
@ -598,6 +617,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 3 becomes second STANDBY (approved but not primary)")
t.Logf(" Expected: Router 1 remains PRIMARY, Router 2 remains first STANDBY")
t.Logf(" Expected: Full HA configuration with 1 PRIMARY + 2 STANDBY routers")
_, err = headscale.ApproveRoutes(
MustFindNode(subRouter3.Hostname(), nodes).GetId(),
[]netip.Prefix{pref},
@ -672,12 +692,14 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.NotEmpty(c, ips, "subRouter1 should have IP addresses")
var expectedIP netip.Addr
for _, ip := range ips {
if ip.Is4() {
expectedIP = ip
break
}
}
assert.True(c, expectedIP.IsValid(), "subRouter1 should have a valid IPv4 address")
assertTracerouteViaIPWithCollect(c, tr, expectedIP)
@ -705,6 +727,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 2 (%s) should automatically become new PRIMARY", subRouter2.Hostname())
t.Logf(" Expected: Router 3 remains STANDBY")
t.Logf(" Expected: Traffic seamlessly fails over to router 2")
err = subRouter1.Down()
require.NoError(t, err)
@ -754,10 +777,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter2.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter2") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 2 after failover")
@ -783,6 +808,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 3 (%s) should become new PRIMARY (last remaining router)", subRouter3.Hostname())
t.Logf(" Expected: With only 1 router left, HA is effectively disabled")
t.Logf(" Expected: Traffic continues through router 3")
err = subRouter2.Down()
require.NoError(t, err)
@ -825,10 +851,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter3.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter3") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 3 after second failover")
@ -853,6 +881,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 3 remains PRIMARY (stability - no unnecessary failover)")
t.Logf(" Expected: Router 1 becomes STANDBY (ready for HA)")
t.Logf(" Expected: HA is restored with 2 routers available")
err = subRouter1.Up()
require.NoError(t, err)
@ -902,10 +931,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter3.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter3") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 3 after router 1 recovery")
@ -932,6 +963,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 1 (%s) remains first STANDBY", subRouter1.Hostname())
t.Logf(" Expected: Router 2 (%s) becomes second STANDBY", subRouter2.Hostname())
t.Logf(" Expected: Full HA restored with all 3 routers online")
err = subRouter2.Up()
require.NoError(t, err)
@ -982,10 +1014,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter3.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter3") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 3 after full recovery")
@ -1067,10 +1101,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter1.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter1") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 1 after route disable")
@ -1153,10 +1189,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter2.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter2") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute goes through router 2 after second route disable")
@ -1182,6 +1220,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 2 (%s) remains PRIMARY (stability - no unnecessary flapping)", subRouter2.Hostname())
t.Logf(" Expected: Router 1 (%s) becomes STANDBY (approved but not primary)", subRouter1.Hostname())
t.Logf(" Expected: HA fully restored with Router 2 PRIMARY and Router 1 STANDBY")
r1Node := MustFindNode(subRouter1.Hostname(), nodes)
_, err = headscale.ApproveRoutes(
r1Node.GetId(),
@ -1237,10 +1276,12 @@ func TestHASubnetRouterFailover(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := subRouter2.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for subRouter2") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, propagationTime, 200*time.Millisecond, "Verifying traceroute still goes through router 2 after route re-enable")
@ -1266,6 +1307,7 @@ func TestHASubnetRouterFailover(t *testing.T) {
t.Logf(" Expected: Router 2 (%s) remains PRIMARY (stability preferred)", subRouter2.Hostname())
t.Logf(" Expected: Routers 1 & 3 are both STANDBY")
t.Logf(" Expected: Full HA restored with all 3 routers available")
r3Node := MustFindNode(subRouter3.Hostname(), nodes)
_, err = headscale.ApproveRoutes(
r3Node.GetId(),
@ -1315,6 +1357,7 @@ func TestSubnetRouteACL(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
@ -1362,6 +1405,7 @@ func TestSubnetRouteACL(t *testing.T) {
sort.SliceStable(allClients, func(i, j int) bool {
statusI := allClients[i].MustStatus()
statusJ := allClients[j].MustStatus()
return statusI.Self.ID < statusJ.Self.ID
})
@ -1391,15 +1435,20 @@ func TestSubnetRouteACL(t *testing.T) {
// Wait for route advertisements to propagate to the server
var nodes []*v1.Node
require.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, nodes, 2)
// Find the node that should have the route by checking node IDs
var routeNode *v1.Node
var otherNode *v1.Node
var (
routeNode *v1.Node
otherNode *v1.Node
)
for _, node := range nodes {
nodeIDStr := strconv.FormatUint(node.GetId(), 10)
if _, shouldHaveRoute := expectedRoutes[nodeIDStr]; shouldHaveRoute {
@ -1462,6 +1511,7 @@ func TestSubnetRouteACL(t *testing.T) {
srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey]
assert.NotNil(c, srs1PeerStatus, "Router 1 peer should exist")
if srs1PeerStatus == nil {
return
}
@ -1552,7 +1602,7 @@ func TestSubnetRouteACL(t *testing.T) {
func TestEnablingExitRoutes(t *testing.T) {
IntegrationSkip(t)
user := "user2"
user := "user2" //nolint:goconst // test-specific value, not related to userToDelete constant
spec := ScenarioSpec{
NodesPerUser: 2,
@ -1560,6 +1610,7 @@ func TestEnablingExitRoutes(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoErrorf(t, err, "failed to create scenario")
defer scenario.ShutdownAssertNoPanics(t)
@ -1581,8 +1632,10 @@ func TestEnablingExitRoutes(t *testing.T) {
requireNoErrSync(t, err)
var nodes []*v1.Node
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, nodes, 2)
@ -1640,6 +1693,7 @@ func TestEnablingExitRoutes(t *testing.T) {
peerStatus := status.Peer[peerKey]
assert.NotNil(c, peerStatus.AllowedIPs)
if peerStatus.AllowedIPs != nil {
assert.Len(c, peerStatus.AllowedIPs.AsSlice(), 4)
assert.Contains(c, peerStatus.AllowedIPs.AsSlice(), tsaddr.AllIPv4())
@ -1670,6 +1724,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
@ -1700,10 +1755,12 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
if s.User[s.Self.UserID].LoginName == "user1@test.no" {
user1c = c
}
if s.User[s.Self.UserID].LoginName == "user2@test.no" {
user2c = c
}
}
require.NotNil(t, user1c)
require.NotNil(t, user2c)
@ -1720,6 +1777,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
// Wait for route advertisements to propagate to NodeStore
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(ct, err)
assert.Len(ct, nodes, 2)
@ -1750,6 +1808,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
// Wait for route state changes to propagate to nodes
assert.EventuallyWithT(t, func(c *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(c, err)
assert.Len(c, nodes, 2)
@ -1767,6 +1826,7 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *pref)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*pref})
}
}, 10*time.Second, 500*time.Millisecond, "routes should be visible to client")
@ -1793,10 +1853,12 @@ func TestSubnetRouterMultiNetwork(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := user2c.Traceroute(webip)
assert.NoError(c, err)
ip, err := user1c.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for user1c") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, 5*time.Second, 200*time.Millisecond, "Verifying traceroute goes through subnet router")
}
@ -1817,6 +1879,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
@ -1844,10 +1907,12 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
if s.User[s.Self.UserID].LoginName == "user1@test.no" {
user1c = c
}
if s.User[s.Self.UserID].LoginName == "user2@test.no" {
user2c = c
}
}
require.NotNil(t, user1c)
require.NotNil(t, user2c)
@ -1864,6 +1929,7 @@ func TestSubnetRouterMultiNetworkExitNode(t *testing.T) {
// Wait for route advertisements to propagate to NodeStore
assert.EventuallyWithT(t, func(ct *assert.CollectT) {
var err error
nodes, err = headscale.ListNodes()
assert.NoError(ct, err)
assert.Len(ct, nodes, 2)
@ -1946,6 +2012,7 @@ func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
return node
}
}
panic("node not found")
}
@ -1965,6 +2032,8 @@ func MustFindNode(hostname string, nodes []*v1.Node) *v1.Node {
// - Verify that peers can no longer use node
// - Policy is changed back to auto approve route, check that routes already existing is approved.
// - Verify that routes can now be seen by peers.
//
//nolint:gocyclo // complex multi-network auto-approve test scenario
func TestAutoApproveMultiNetwork(t *testing.T) {
IntegrationSkip(t)
@ -2229,10 +2298,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
}
scenario, err := NewScenario(tt.spec)
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
var nodes []*v1.Node
opts := []hsic.Option{
hsic.WithTestName("autoapprovemulti"),
hsic.WithEmbeddedDERPServerOnly(),
@ -2259,7 +2330,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
preAuthKeyTags = []string{tt.approver}
if tt.withURL {
// For webauth, only user1 can request tags (per tagOwners policy)
webauthTagUser = "user1"
webauthTagUser = "user1" //nolint:goconst // test value, not a constant
}
}
@ -2288,6 +2359,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
// Add the Docker network route to the auto-approvers
// Keep existing auto-approvers (like bigRoute) in place
var approvers policyv2.AutoApprovers
switch {
case strings.HasPrefix(tt.approver, "tag:"):
approvers = append(approvers, tagApprover(tt.approver))
@ -2356,6 +2428,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
} else {
pak, err = scenario.CreatePreAuthKey(userMap["user1"].GetId(), false, false)
}
require.NoError(t, err)
err = routerUsernet1.Login(headscale.GetEndpoint(), pak.GetKey())
@ -2447,11 +2520,13 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
t.Logf("Client %s sees %d peers", client.Hostname(), len(status.Peers()))
routerPeerFound := false
for _, peerKey := range status.Peers() {
peerStatus := status.Peer[peerKey]
if peerStatus.ID == routerUsernet1ID.StableID() {
routerPeerFound = true
t.Logf("Client sees router peer %s (ID=%s): AllowedIPs=%v, PrimaryRoutes=%v",
peerStatus.HostName,
peerStatus.ID,
@ -2459,9 +2534,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
peerStatus.PrimaryRoutes)
assert.NotNil(c, peerStatus.PrimaryRoutes)
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
@ -2498,10 +2575,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := routerUsernet1.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for routerUsernet1") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, assertTimeout, 200*time.Millisecond, "Verifying traceroute goes through auto-approved router")
@ -2538,9 +2617,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
if peerStatus.ID == routerUsernet1ID.StableID() {
assert.NotNil(c, peerStatus.PrimaryRoutes)
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
@ -2560,10 +2641,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := routerUsernet1.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for routerUsernet1") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, assertTimeout, 200*time.Millisecond, "Verifying traceroute still goes through router after policy change")
@ -2597,6 +2680,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
// Add the route back to the auto approver in the policy, the route should
// now become available again.
var newApprovers policyv2.AutoApprovers
switch {
case strings.HasPrefix(tt.approver, "tag:"):
newApprovers = append(newApprovers, tagApprover(tt.approver))
@ -2630,9 +2714,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
if peerStatus.ID == routerUsernet1ID.StableID() {
assert.NotNil(c, peerStatus.PrimaryRoutes)
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
@ -2652,10 +2738,12 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := client.Traceroute(webip)
assert.NoError(c, err)
ip, err := routerUsernet1.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for routerUsernet1") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, assertTimeout, 200*time.Millisecond, "Verifying traceroute goes through router after re-approval")
@ -2691,11 +2779,13 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
} else if peerStatus.ID == "2" {
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), subRoute)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{subRoute})
} else {
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
@ -2733,9 +2823,11 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
if peerStatus.ID == routerUsernet1ID.StableID() {
assert.NotNil(c, peerStatus.PrimaryRoutes)
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
} else {
requirePeerSubnetRoutesWithCollect(c, peerStatus, nil)
@ -2773,6 +2865,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
if peerStatus.PrimaryRoutes != nil {
assert.Contains(c, peerStatus.PrimaryRoutes.AsSlice(), *route)
}
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{*route})
} else if peerStatus.ID == "3" {
requirePeerSubnetRoutesWithCollect(c, peerStatus, []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()})
@ -2791,7 +2884,7 @@ func TestAutoApproveMultiNetwork(t *testing.T) {
func assertTracerouteViaIPWithCollect(c *assert.CollectT, tr util.Traceroute, ip netip.Addr) {
assert.NotNil(c, tr)
assert.True(c, tr.Success)
assert.NoError(c, tr.Err)
assert.NoError(c, tr.Err) //nolint:testifylint // using assert.CollectT
assert.NotEmpty(c, tr.Route)
// Since we're inside EventuallyWithT, we can't use require.Greater with t
// but assert.NotEmpty above ensures len(tr.Route) > 0
@ -2805,12 +2898,15 @@ func SortPeerStatus(a, b *ipnstate.PeerStatus) int {
}
func printCurrentRouteMap(t *testing.T, routers ...*ipnstate.PeerStatus) {
t.Helper()
t.Logf("== Current routing map ==")
slices.SortFunc(routers, SortPeerStatus)
for _, router := range routers {
got := filterNonRoutes(router)
t.Logf(" Router %s (%s) is serving:", router.HostName, router.ID)
t.Logf(" AllowedIPs: %v", got)
if router.PrimaryRoutes != nil {
t.Logf(" PrimaryRoutes: %v", router.PrimaryRoutes.AsSlice())
}
@ -2823,6 +2919,7 @@ func filterNonRoutes(status *ipnstate.PeerStatus) []netip.Prefix {
if tsaddr.IsExitRoute(p) {
return true
}
return !slices.ContainsFunc(status.TailscaleIPs, p.Contains)
})
}
@ -2874,6 +2971,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
}
scenario, err := NewScenario(spec)
require.NoErrorf(t, err, "failed to create scenario: %s", err)
defer scenario.ShutdownAssertNoPanics(t)
@ -3014,6 +3112,7 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
// List nodes and verify the router has 3 available routes
var err error
nodes, err := headscale.NodesByUser()
assert.NoError(c, err)
assert.Len(c, nodes, 2)
@ -3049,10 +3148,12 @@ func TestSubnetRouteACLFiltering(t *testing.T) {
assert.EventuallyWithT(t, func(c *assert.CollectT) {
tr, err := nodeClient.Traceroute(webip)
assert.NoError(c, err)
ip, err := routerClient.IPv4()
if !assert.NoError(c, err, "failed to get IPv4 for routerClient") {
return
}
assertTracerouteViaIPWithCollect(c, tr, ip)
}, 60*time.Second, 200*time.Millisecond, "Verifying traceroute goes through router")
}

View file

@ -96,7 +96,7 @@ type User struct {
type Scenario struct {
// TODO(kradalby): support multiple headcales for later, currently only
// use one.
controlServers *xsync.MapOf[string, ControlServer]
controlServers *xsync.Map[string, ControlServer]
derpServers []*dsic.DERPServerInContainer
users map[string]*User
@ -169,8 +169,8 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
// Opportunity to clean up unreferenced networks.
// This might be a no op, but it is worth a try as we sometime
// dont clean up nicely after ourselves.
dockertestutil.CleanUnreferencedNetworks(pool)
dockertestutil.CleanImagesInCI(pool)
_ = dockertestutil.CleanUnreferencedNetworks(pool)
_ = dockertestutil.CleanImagesInCI(pool)
if spec.MaxWait == 0 {
pool.MaxWait = dockertestMaxWait()
@ -180,7 +180,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
testHashPrefix := "hs-" + util.MustGenerateRandomStringDNSSafe(scenarioHashLength)
s := &Scenario{
controlServers: xsync.NewMapOf[string, ControlServer](),
controlServers: xsync.NewMap[string, ControlServer](),
users: make(map[string]*User),
pool: pool,
@ -191,9 +191,11 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
}
var userToNetwork map[string]*dockertest.Network
if spec.Networks != nil || len(spec.Networks) != 0 {
for name, users := range s.spec.Networks {
networkName := testHashPrefix + "-" + name
network, err := s.AddNetwork(networkName)
if err != nil {
return nil, err
@ -201,8 +203,9 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
for _, user := range users {
if n2, ok := userToNetwork[user]; ok {
return nil, fmt.Errorf("users can only have nodes placed in one network: %s into %s but already in %s", user, network.Network.Name, n2.Network.Name)
return nil, fmt.Errorf("users can only have nodes placed in one network: %s into %s but already in %s", user, network.Network.Name, n2.Network.Name) //nolint:err113
}
mak.Set(&userToNetwork, user, network)
}
}
@ -219,6 +222,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
if err != nil {
return nil, err
}
mak.Set(&s.extraServices, s.prefixedNetworkName(network), append(s.extraServices[s.prefixedNetworkName(network)], svc))
}
}
@ -230,6 +234,7 @@ func NewScenario(spec ScenarioSpec) (*Scenario, error) {
if spec.OIDCAccessTTL != 0 {
ttl = spec.OIDCAccessTTL
}
err = s.runMockOIDC(ttl, spec.OIDCUsers)
if err != nil {
return nil, err
@ -268,13 +273,14 @@ func (s *Scenario) Networks() []*dockertest.Network {
if len(s.networks) == 0 {
panic("Scenario.Networks called with empty network list")
}
return xmaps.Values(s.networks)
}
func (s *Scenario) Network(name string) (*dockertest.Network, error) {
net, ok := s.networks[s.prefixedNetworkName(name)]
if !ok {
return nil, fmt.Errorf("no network named: %s", name)
return nil, fmt.Errorf("no network named: %s", name) //nolint:err113
}
return net, nil
@ -283,11 +289,11 @@ func (s *Scenario) Network(name string) (*dockertest.Network, error) {
func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
net, ok := s.networks[s.prefixedNetworkName(name)]
if !ok {
return nil, fmt.Errorf("no network named: %s", name)
return nil, fmt.Errorf("no network named: %s", name) //nolint:err113
}
if len(net.Network.IPAM.Config) == 0 {
return nil, fmt.Errorf("no IPAM config found in network: %s", name)
return nil, fmt.Errorf("no IPAM config found in network: %s", name) //nolint:err113
}
pref, err := netip.ParsePrefix(net.Network.IPAM.Config[0].Subnet)
@ -301,15 +307,17 @@ func (s *Scenario) SubnetOfNetwork(name string) (*netip.Prefix, error) {
func (s *Scenario) Services(name string) ([]*dockertest.Resource, error) {
res, ok := s.extraServices[s.prefixedNetworkName(name)]
if !ok {
return nil, fmt.Errorf("no network named: %s", name)
return nil, fmt.Errorf("no network named: %s", name) //nolint:err113
}
return res, nil
}
func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
defer dockertestutil.CleanUnreferencedNetworks(s.pool)
defer dockertestutil.CleanImagesInCI(s.pool)
t.Helper()
defer func() { _ = dockertestutil.CleanUnreferencedNetworks(s.pool) }()
defer func() { _ = dockertestutil.CleanImagesInCI(s.pool) }()
s.controlServers.Range(func(_ string, control ControlServer) bool {
stdoutPath, stderrPath, err := control.Shutdown()
@ -334,9 +342,11 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
})
s.mu.Lock()
for userName, user := range s.users {
for _, client := range user.Clients {
log.Printf("removing client %s in user %s", client.Hostname(), userName)
stdoutPath, stderrPath, err := client.Shutdown()
if err != nil {
log.Printf("tearing down client: %s", err)
@ -353,6 +363,7 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
}
}
}
s.mu.Unlock()
for _, derp := range s.derpServers {
@ -373,13 +384,16 @@ func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) {
if s.mockOIDC.r != nil {
s.mockOIDC.r.Close()
if err := s.mockOIDC.r.Close(); err != nil {
err := s.mockOIDC.r.Close()
if err != nil {
log.Printf("tearing down oidc server: %s", err)
}
}
for _, network := range s.networks {
if err := network.Close(); err != nil {
err := network.Close()
if err != nil {
log.Printf("tearing down network: %s", err)
}
}
@ -395,7 +409,7 @@ func (s *Scenario) Shutdown() {
// Users returns the name of all users associated with the Scenario.
func (s *Scenario) Users() []string {
users := make([]string, 0)
users := make([]string, 0, len(s.users))
for user := range s.users {
users = append(users, user)
}
@ -466,7 +480,7 @@ func (s *Scenario) CreatePreAuthKey(
reusable bool,
ephemeral bool,
) (*v1.PreAuthKey, error) {
if headscale, err := s.Headscale(); err == nil {
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
key, err := headscale.CreateAuthKey(user, reusable, ephemeral)
if err != nil {
return nil, fmt.Errorf("creating user: %w", err)
@ -518,7 +532,7 @@ func (s *Scenario) CreatePreAuthKeyWithTags(
// 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) {
if headscale, err := s.Headscale(); err == nil {
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
u, err := headscale.CreateUser(user)
if err != nil {
return nil, fmt.Errorf("creating user: %w", err)
@ -552,6 +566,7 @@ func (s *Scenario) CreateTailscaleNode(
s.mu.Lock()
defer s.mu.Unlock()
opts = append(opts,
tsic.WithCACert(cert),
tsic.WithHeadscaleName(hostname),
@ -591,6 +606,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
) error {
if user, ok := s.users[userStr]; ok {
var versions []string
for i := range count {
version := requestedVersion
if requestedVersion == "all" {
@ -600,6 +616,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
version = MustTestVersions[i%len(MustTestVersions)]
}
}
versions = append(versions, version)
headscale, err := s.Headscale()
@ -623,6 +640,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
extraHosts := []string{hostname + ":" + headscaleIP}
s.mu.Lock()
opts = append(opts,
tsic.WithCACert(cert),
tsic.WithHeadscaleName(hostname),
@ -639,6 +657,7 @@ func (s *Scenario) CreateTailscaleNodesInUser(
opts...,
)
s.mu.Unlock()
if err != nil {
return fmt.Errorf(
"creating tailscale node: %w",
@ -656,13 +675,17 @@ func (s *Scenario) CreateTailscaleNodesInUser(
}
s.mu.Lock()
user.Clients[tsClient.Hostname()] = tsClient
s.mu.Unlock()
return nil
})
}
if err := user.createWaitGroup.Wait(); err != nil {
err := user.createWaitGroup.Wait()
if err != nil {
return err
}
@ -682,12 +705,14 @@ func (s *Scenario) RunTailscaleUp(
if user, ok := s.users[userStr]; ok {
for _, client := range user.Clients {
c := client
user.joinWaitGroup.Go(func() error {
return c.Login(loginServer, authKey)
})
}
if err := user.joinWaitGroup.Wait(); err != nil {
err := user.joinWaitGroup.Wait()
if err != nil {
return err
}
@ -749,11 +774,14 @@ func (s *Scenario) WaitForTailscaleSyncPerUser(timeout, retryInterval time.Durat
for _, client := range user.Clients {
c := client
expectedCount := expectedPeers
user.syncWaitGroup.Go(func() error {
return c.WaitForPeers(expectedCount, timeout, retryInterval)
})
}
if err := user.syncWaitGroup.Wait(); err != nil {
err := user.syncWaitGroup.Wait()
if err != nil {
allErrors = append(allErrors, err)
}
}
@ -773,11 +801,14 @@ func (s *Scenario) WaitForTailscaleSyncWithPeerCount(peerCount int, timeout, ret
for _, user := range s.users {
for _, client := range user.Clients {
c := client
user.syncWaitGroup.Go(func() error {
return c.WaitForPeers(peerCount, timeout, retryInterval)
})
}
if err := user.syncWaitGroup.Wait(); err != nil {
err := user.syncWaitGroup.Wait()
if err != nil {
allErrors = append(allErrors, err)
}
}
@ -871,6 +902,7 @@ func (s *Scenario) createHeadscaleEnvWithTags(
} else {
key, err = s.CreatePreAuthKey(u.GetId(), true, false)
}
if err != nil {
return err
}
@ -887,9 +919,11 @@ func (s *Scenario) createHeadscaleEnvWithTags(
func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error {
log.Printf("running tailscale up for user %s", userStr)
if user, ok := s.users[userStr]; ok {
for _, client := range user.Clients {
tsc := client
user.joinWaitGroup.Go(func() error {
loginURL, err := tsc.LoginWithURL(loginServer)
if err != nil {
@ -904,7 +938,7 @@ func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error {
// If the URL is not a OIDC URL, then we need to
// run the register command to fully log in the client.
if !strings.Contains(loginURL.String(), "/oidc/") {
s.runHeadscaleRegister(userStr, body)
_ = s.runHeadscaleRegister(userStr, body)
}
return nil
@ -913,7 +947,8 @@ func (s *Scenario) RunTailscaleUpWithURL(userStr, loginServer string) error {
log.Printf("client %s is ready", client.Hostname())
}
if err := user.joinWaitGroup.Wait(); err != nil {
err := user.joinWaitGroup.Wait()
if err != nil {
return err
}
@ -945,6 +980,7 @@ func newDebugJar() (*debugJar, error) {
if err != nil {
return nil, err
}
return &debugJar{
inner: jar,
store: make(map[string]map[string]map[string]*http.Cookie),
@ -961,20 +997,25 @@ func (j *debugJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
if c == nil || c.Name == "" {
continue
}
domain := c.Domain
if domain == "" {
domain = u.Hostname()
}
path := c.Path
if path == "" {
path = "/"
}
if _, ok := j.store[domain]; !ok {
j.store[domain] = make(map[string]map[string]*http.Cookie)
}
if _, ok := j.store[domain][path]; !ok {
j.store[domain][path] = make(map[string]*http.Cookie)
}
j.store[domain][path][c.Name] = copyCookie(c)
}
}
@ -989,8 +1030,10 @@ func (j *debugJar) Dump(w io.Writer) {
for domain, paths := range j.store {
fmt.Fprintf(w, "Domain: %s\n", domain)
for path, byName := range paths {
fmt.Fprintf(w, " Path: %s\n", path)
for _, c := range byName {
fmt.Fprintf(
w, " %s=%s; Expires=%v; Secure=%v; HttpOnly=%v; SameSite=%v\n",
@ -1046,15 +1089,17 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
error,
) {
if hc == nil {
return "", nil, fmt.Errorf("%s http client is nil", hostname)
return "", nil, fmt.Errorf("%s http client is nil", hostname) //nolint:err113
}
if loginURL == nil {
return "", nil, fmt.Errorf("%s login url is nil", hostname)
return "", nil, fmt.Errorf("%s login url is nil", hostname) //nolint:err113
}
log.Printf("%s logging in with url: %s", hostname, loginURL.String())
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, loginURL.String(), nil)
if err != nil {
return "", nil, fmt.Errorf("%s creating http request: %w", hostname, err)
@ -1066,6 +1111,7 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
return http.ErrUseLastResponse
}
}
defer func() {
hc.CheckRedirect = originalRedirect
}()
@ -1080,6 +1126,7 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
if err != nil {
return "", nil, fmt.Errorf("%s reading response body: %w", hostname, err)
}
body := string(bodyBytes)
var redirectURL *url.URL
@ -1093,13 +1140,13 @@ func doLoginURLWithClient(hostname string, loginURL *url.URL, hc *http.Client, f
if followRedirects && resp.StatusCode != http.StatusOK {
log.Printf("body: %s", body)
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode)
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode) //nolint:err113
}
if resp.StatusCode >= http.StatusBadRequest {
log.Printf("body: %s", body)
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode)
return body, redirectURL, fmt.Errorf("%s unexpected status code %d", hostname, resp.StatusCode) //nolint:err113
}
if hc.Jar != nil {
@ -1117,7 +1164,7 @@ var errParseAuthPage = errors.New("parsing auth page")
func (s *Scenario) runHeadscaleRegister(userStr string, body string) error {
// see api.go HTML template
codeSep := strings.Split(string(body), "</code>")
codeSep := strings.Split(body, "</code>")
if len(codeSep) != 2 {
return errParseAuthPage
}
@ -1126,11 +1173,12 @@ func (s *Scenario) runHeadscaleRegister(userStr string, body string) error {
if len(keySep) != 2 {
return errParseAuthPage
}
key := keySep[1]
key = strings.SplitN(key, " ", 2)[0]
log.Printf("registering node %s", key)
if headscale, err := s.Headscale(); err == nil {
if headscale, err := s.Headscale(); err == nil { //nolint:noinlineerr
_, err = headscale.Execute(
[]string{"headscale", "nodes", "register", "--user", userStr, "--key", key},
)
@ -1154,6 +1202,7 @@ func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
noTls := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint
}
resp, err := noTls.RoundTrip(req)
if err != nil {
return nil, err
@ -1173,12 +1222,14 @@ func (t LoggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
// in a Scenario.
func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
var ips []netip.Addr
if ns, ok := s.users[user]; ok {
for _, client := range ns.Clients {
clientIps, err := client.IPs()
if err != nil {
return ips, fmt.Errorf("getting IPs: %w", err)
}
ips = append(ips, clientIps...)
}
@ -1191,6 +1242,7 @@ func (s *Scenario) GetIPs(user string) ([]netip.Addr, error) {
// GetClients returns all TailscaleClients associated with a User in a Scenario.
func (s *Scenario) GetClients(user string) ([]TailscaleClient, error) {
var clients []TailscaleClient
if ns, ok := s.users[user]; ok {
for _, client := range ns.Clients {
clients = append(clients, client)
@ -1290,11 +1342,14 @@ func (s *Scenario) WaitForTailscaleLogout() error {
for _, user := range s.users {
for _, client := range user.Clients {
c := client
user.syncWaitGroup.Go(func() error {
return c.WaitForNeedsLogin(integrationutil.PeerSyncTimeout())
})
}
if err := user.syncWaitGroup.Wait(); err != nil {
err := user.syncWaitGroup.Wait()
if err != nil {
return err
}
}
@ -1361,6 +1416,7 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse
if err != nil {
log.Fatalf("finding open port: %s", err)
}
portNotation := fmt.Sprintf("%d/tcp", port)
hash, _ := util.GenerateRandomStringDNSSafe(hsicOIDCMockHashLength)
@ -1405,7 +1461,7 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse
// Add integration test labels if running under hi tool
dockertestutil.DockerAddIntegrationLabels(mockOidcOptions, "oidc")
if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions(
if pmockoidc, err := s.pool.BuildAndRunWithBuildOptions( //nolint:noinlineerr
headscaleBuildOptions,
mockOidcOptions,
dockertestutil.DockerRestartPolicy); err == nil {
@ -1421,9 +1477,10 @@ func (s *Scenario) runMockOIDC(accessTTL time.Duration, users []mockoidc.MockUse
ipAddr := s.mockOIDC.r.GetIPInNetwork(network)
log.Println("Waiting for headscale mock oidc to be ready for tests")
hostEndpoint := net.JoinHostPort(ipAddr, strconv.Itoa(port))
if err := s.pool.Retry(func() error {
if err := s.pool.Retry(func() error { //nolint:noinlineerr
oidcConfigURL := fmt.Sprintf("http://%s/oidc/.well-known/openid-configuration", hostEndpoint)
httpClient := &http.Client{}
ctx := context.Background()
@ -1468,14 +1525,13 @@ func Webservice(s *Scenario, networkName string) (*dockertest.Resource, error) {
// log.Fatalf("finding open port: %s", err)
// }
// portNotation := fmt.Sprintf("%d/tcp", port)
hash := util.MustGenerateRandomStringDNSSafe(hsicOIDCMockHashLength)
hostname := "hs-webservice-" + hash
network, ok := s.networks[s.prefixedNetworkName(networkName)]
if !ok {
return nil, fmt.Errorf("network does not exist: %s", networkName)
return nil, fmt.Errorf("network does not exist: %s", networkName) //nolint:err113
}
webOpts := &dockertest.RunOptions{

View file

@ -35,6 +35,7 @@ func TestHeadscale(t *testing.T) {
user := "test-space"
scenario, err := NewScenario(ScenarioSpec{})
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)
@ -83,6 +84,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) {
count := 1
scenario, err := NewScenario(ScenarioSpec{})
require.NoError(t, err)
defer scenario.ShutdownAssertNoPanics(t)

View file

@ -493,7 +493,7 @@ func assertSSHTimeout(t *testing.T, client TailscaleClient, peer TailscaleClient
func assertSSHNoAccessStdError(t *testing.T, err error, stderr string) {
t.Helper()
assert.Error(t, err)
require.Error(t, err)
if !isSSHNoAccessStdError(stderr) {
t.Errorf("expected stderr output suggesting access denied, got: %s", stderr)

View file

@ -2502,7 +2502,7 @@ func assertNetmapSelfHasTagsWithCollect(c *assert.CollectT, client TailscaleClie
var actualTagsSlice []string
if nm.SelfNode.Valid() {
for _, tag := range nm.SelfNode.Tags().All() {
for _, tag := range nm.SelfNode.Tags().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
actualTagsSlice = append(actualTagsSlice, tag)
}
}
@ -2647,7 +2647,7 @@ func TestTagsIssue2978ReproTagReplacement(t *testing.T) {
var netmapTagsAfterFirstCall []string
if nmErr == nil && nm != nil && nm.SelfNode.Valid() {
for _, tag := range nm.SelfNode.Tags().All() {
for _, tag := range nm.SelfNode.Tags().All() { //nolint:unqueryvet // not SQLBoiler, tailcfg iterator
netmapTagsAfterFirstCall = append(netmapTagsAfterFirstCall, tag)
}
}

View file

@ -52,8 +52,6 @@ var (
errTailscaleNotLoggedIn = errors.New("tailscale not logged in")
errTailscaleWrongPeerCount = errors.New("wrong peer count")
errTailscaleCannotUpWithoutAuthkey = errors.New("cannot up without authkey")
errTailscaleNotConnected = errors.New("tailscale not connected")
errTailscaledNotReadyForLogin = errors.New("tailscaled not ready for login")
errInvalidClientConfig = errors.New("verifiably invalid client config requested")
errInvalidTailscaleImageFormat = errors.New("invalid HEADSCALE_INTEGRATION_TAILSCALE_IMAGE format, expected repository:tag")
errTailscaleImageRequiredInCI = errors.New("HEADSCALE_INTEGRATION_TAILSCALE_IMAGE must be set in CI for HEAD version")
@ -297,6 +295,8 @@ func (t *TailscaleInContainer) buildEntrypoint() []string {
}
// New returns a new TailscaleInContainer instance.
//
//nolint:gocyclo // complex container setup with many options
func New(
pool *dockertest.Pool,
version string,
@ -338,7 +338,7 @@ func New(
}
if tsic.network == nil {
return nil, fmt.Errorf("no network set, called from: \n%s", string(debug.Stack()))
return nil, fmt.Errorf("no network set, called from: \n%s", string(debug.Stack())) //nolint:err113
}
tailscaleOptions := &dockertest.RunOptions{
@ -586,7 +586,7 @@ func (t *TailscaleInContainer) Version() string {
return t.version
}
// ID returns the Docker container ID of the TailscaleInContainer
// ContainerID returns the Docker container ID of the TailscaleInContainer
// instance.
func (t *TailscaleInContainer) ContainerID() string {
return t.container.Container.ID
@ -621,7 +621,7 @@ func (t *TailscaleInContainer) Execute(
return stdout, stderr, nil
}
// Retrieve container logs.
// Logs retrieves the container logs.
func (t *TailscaleInContainer) Logs(stdout, stderr io.Writer) error {
return dockertestutil.WriteLog(
t.pool,
@ -673,7 +673,7 @@ func (t *TailscaleInContainer) Login(
) error {
command := t.buildLoginCommand(loginServer, authKey)
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil { //nolint:noinlineerr
return fmt.Errorf(
"%s failed to join tailscale client (%s): %w",
t.hostname,
@ -685,11 +685,11 @@ func (t *TailscaleInContainer) Login(
return nil
}
// Up runs the login routine on the given Tailscale instance.
// LoginWithURL runs the login routine on the given Tailscale instance.
// This login mechanism uses web + command line flow for authentication.
func (t *TailscaleInContainer) LoginWithURL(
loginServer string,
) (loginURL *url.URL, err error) {
) (*url.URL, error) {
command := t.buildLoginCommand(loginServer, "")
stdout, stderr, err := t.Execute(command)
@ -703,7 +703,7 @@ func (t *TailscaleInContainer) LoginWithURL(
}
}()
loginURL, err = util.ParseLoginURLFromCLILogin(stdout + stderr)
loginURL, err := util.ParseLoginURLFromCLILogin(stdout + stderr)
if err != nil {
return nil, err
}
@ -713,14 +713,14 @@ func (t *TailscaleInContainer) LoginWithURL(
// Logout runs the logout routine on the given Tailscale instance.
func (t *TailscaleInContainer) Logout() error {
stdout, stderr, err := t.Execute([]string{"tailscale", "logout"})
_, _, err := t.Execute([]string{"tailscale", "logout"})
if err != nil {
return err
}
stdout, stderr, _ = t.Execute([]string{"tailscale", "status"})
stdout, stderr, _ := t.Execute([]string{"tailscale", "status"})
if !strings.Contains(stdout+stderr, "Logged out.") {
return fmt.Errorf("logging out, stdout: %s, stderr: %s", stdout, stderr)
return fmt.Errorf("logging out, stdout: %s, stderr: %s", stdout, stderr) //nolint:err113
}
return t.waitForBackendState("NeedsLogin", integrationutil.PeerSyncTimeout())
@ -759,14 +759,14 @@ func (t *TailscaleInContainer) Restart() error {
return nil
}
// Helper that runs `tailscale up` with no arguments.
// Up runs `tailscale up` with no arguments.
func (t *TailscaleInContainer) Up() error {
command := []string{
"tailscale",
"up",
}
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil { //nolint:noinlineerr
return fmt.Errorf(
"%s failed to bring tailscale client up (%s): %w",
t.hostname,
@ -778,14 +778,14 @@ func (t *TailscaleInContainer) Up() error {
return nil
}
// Helper that runs `tailscale down` with no arguments.
// Down runs `tailscale down` with no arguments.
func (t *TailscaleInContainer) Down() error {
command := []string{
"tailscale",
"down",
}
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil {
if _, _, err := t.Execute(command, dockertestutil.ExecuteCommandTimeout(dockerExecuteTimeout)); err != nil { //nolint:noinlineerr
return fmt.Errorf(
"%s failed to bring tailscale client down (%s): %w",
t.hostname,
@ -832,7 +832,7 @@ func (t *TailscaleInContainer) IPs() ([]netip.Addr, error) {
}
if len(ips) == 0 {
return nil, fmt.Errorf("no IPs returned yet for %s", t.hostname)
return nil, fmt.Errorf("no IPs returned yet for %s", t.hostname) //nolint:err113
}
return ips, nil
@ -866,7 +866,7 @@ func (t *TailscaleInContainer) IPv4() (netip.Addr, error) {
}
}
return netip.Addr{}, fmt.Errorf("no IPv4 address found for %s", t.hostname)
return netip.Addr{}, fmt.Errorf("no IPv4 address found for %s", t.hostname) //nolint:err113
}
func (t *TailscaleInContainer) MustIPv4() netip.Addr {
@ -908,7 +908,7 @@ func (t *TailscaleInContainer) Status(save ...bool) (*ipnstate.Status, error) {
return nil, fmt.Errorf("unmarshalling tailscale status: %w", err)
}
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_status.json", t.hostname), []byte(result), 0o755)
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_status.json", t.hostname), []byte(result), 0o755) //nolint:gosec // test infrastructure log files
if err != nil {
return nil, fmt.Errorf("status netmap to /tmp/control: %w", err)
}
@ -968,7 +968,7 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
return nil, fmt.Errorf("unmarshalling tailscale netmap: %w", err)
}
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_netmap.json", t.hostname), []byte(result), 0o755)
err = os.WriteFile(fmt.Sprintf("/tmp/control/%s_netmap.json", t.hostname), []byte(result), 0o755) //nolint:gosec // test infrastructure log files
if err != nil {
return nil, fmt.Errorf("saving netmap to /tmp/control: %w", err)
}
@ -1001,6 +1001,8 @@ func (t *TailscaleInContainer) Netmap() (*netmap.NetworkMap, error) {
// watchIPN watches `tailscale debug watch-ipn` for a ipn.Notify object until
// it gets one that has a netmap.NetworkMap.
//
//nolint:unused
func (t *TailscaleInContainer) watchIPN(ctx context.Context) (*ipn.Notify, error) {
pr, pw := io.Pipe()
@ -1211,7 +1213,7 @@ func (t *TailscaleInContainer) waitForBackendState(state string, timeout time.Du
for {
select {
case <-ctx.Done():
return fmt.Errorf("timeout waiting for backend state %s on %s after %v", state, t.hostname, timeout)
return fmt.Errorf("timeout waiting for backend state %s on %s after %v", state, t.hostname, timeout) //nolint:err113
case <-ticker.C:
status, err := t.Status()
if err != nil {
@ -1256,7 +1258,7 @@ func (t *TailscaleInContainer) WaitForPeers(expected int, timeout, retryInterval
return fmt.Errorf("timeout waiting for %d peers on %s after %v, errors: %w", expected, t.hostname, timeout, multierr.New(lastErrs...))
}
return fmt.Errorf("timeout waiting for %d peers on %s after %v", expected, t.hostname, timeout)
return fmt.Errorf("timeout waiting for %d peers on %s after %v", expected, t.hostname, timeout) //nolint:err113
case <-ticker.C:
status, err := t.Status()
if err != nil {
@ -1284,15 +1286,15 @@ func (t *TailscaleInContainer) WaitForPeers(expected int, timeout, retryInterval
peer := status.Peer[peerKey]
if !peer.Online {
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s is not online", t.hostname, peer.HostName))
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s is not online", t.hostname, peer.HostName)) //nolint:err113
}
if peer.HostName == "" {
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a Hostname", t.hostname, peer.HostName))
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a Hostname", t.hostname, peer.HostName)) //nolint:err113
}
if peer.Relay == "" {
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a DERP", t.hostname, peer.HostName))
peerErrors = append(peerErrors, fmt.Errorf("[%s] peer count correct, but %s does not have a DERP", t.hostname, peer.HostName)) //nolint:err113
}
}
@ -1355,14 +1357,14 @@ func (t *TailscaleInContainer) Ping(hostnameOrIP string, opts ...PingOption) err
opt(&args)
}
command := []string{
command := make([]string, 0, 6)
command = append(command,
"tailscale", "ping",
fmt.Sprintf("--timeout=%s", args.timeout),
fmt.Sprintf("--c=%d", args.count),
"--until-direct=" + strconv.FormatBool(args.direct),
}
command = append(command, hostnameOrIP)
"--until-direct="+strconv.FormatBool(args.direct),
hostnameOrIP,
)
result, _, err := t.Execute(
command,
@ -1566,19 +1568,19 @@ func (t *TailscaleInContainer) ReadFile(path string) ([]byte, error) {
}
if !strings.Contains(path, hdr.Name) {
return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name)
return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name) //nolint:err113
}
if _, err := io.Copy(&out, tr); err != nil {
if _, err := io.Copy(&out, tr); err != nil { //nolint:gosec,noinlineerr // trusted tar from test container
return nil, fmt.Errorf("copying file to buffer: %w", err)
}
// Only support reading the first tile
break
break //nolint:staticcheck // SA4004: intentional - only read first file
}
if out.Len() == 0 {
return nil, errors.New("file is empty")
return nil, errors.New("file is empty") //nolint:err113
}
return out.Bytes(), nil
@ -1591,7 +1593,7 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
}
store := &mem.Store{}
if err = store.LoadFromJSON(state); err != nil {
if err = store.LoadFromJSON(state); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("unmarshalling state file: %w", err)
}
@ -1606,7 +1608,7 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
}
p := &ipn.Prefs{}
if err = json.Unmarshal(currentProfile, &p); err != nil {
if err = json.Unmarshal(currentProfile, &p); err != nil { //nolint:noinlineerr
return nil, fmt.Errorf("unmarshalling current profile state: %w", err)
}
@ -1617,7 +1619,7 @@ func (t *TailscaleInContainer) GetNodePrivateKey() (*key.NodePrivate, error) {
// This is useful for verifying that policy changes have propagated to the client.
func (t *TailscaleInContainer) PacketFilter() ([]filter.Match, error) {
if !util.TailscaleVersionNewerOrEqual("1.56", t.version) {
return nil, fmt.Errorf("tsic.PacketFilter() requires Tailscale 1.56+, current version: %s", t.version)
return nil, fmt.Errorf("tsic.PacketFilter() requires Tailscale 1.56+, current version: %s", t.version) //nolint:err113
}
nm, err := t.Netmap()