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

@ -17,7 +17,10 @@ import (
"tailscale.com/types/views"
)
var ErrInvalidAction = errors.New("invalid action")
var (
ErrInvalidAction = errors.New("invalid action")
errSelfInSources = errors.New("autogroup:self cannot be used in sources")
)
// compileFilterRules takes a set of nodes and an ACLPolicy and generates a
// set of Tailscale compatible FilterRules used to allow traffic on clients.
@ -45,9 +48,10 @@ func (pol *Policy) compileFilterRules(
continue
}
protocols, _ := acl.Protocol.parseProtocol()
protocols := acl.Protocol.parseProtocol()
var destPorts []tailcfg.NetPortRange
for _, dest := range acl.Destinations {
// Check if destination is a wildcard - use "*" directly instead of expanding
if _, isWildcard := dest.Alias.(Asterix); isWildcard {
@ -142,14 +146,18 @@ func (pol *Policy) compileFilterRulesForNode(
// It returns a slice of filter rules because when an ACL has both autogroup:self
// and other destinations, they need to be split into separate rules with different
// source filtering logic.
//
//nolint:gocyclo // complex ACL compilation logic
func (pol *Policy) compileACLWithAutogroupSelf(
acl ACL,
users types.Users,
node types.NodeView,
nodes views.Slice[types.NodeView],
) ([]*tailcfg.FilterRule, error) {
var autogroupSelfDests []AliasWithPorts
var otherDests []AliasWithPorts
var (
autogroupSelfDests []AliasWithPorts
otherDests []AliasWithPorts
)
for _, dest := range acl.Destinations {
if ag, ok := dest.Alias.(*AutoGroup); ok && ag.Is(AutoGroupSelf) {
@ -159,14 +167,15 @@ func (pol *Policy) compileACLWithAutogroupSelf(
}
}
protocols, _ := acl.Protocol.parseProtocol()
protocols := acl.Protocol.parseProtocol()
var rules []*tailcfg.FilterRule
var resolvedSrcIPs []*netipx.IPSet
for _, src := range acl.Sources {
if ag, ok := src.(*AutoGroup); ok && ag.Is(AutoGroupSelf) {
return nil, fmt.Errorf("autogroup:self cannot be used in sources")
return nil, errSelfInSources
}
ips, err := src.Resolve(pol, users, nodes)
@ -188,6 +197,7 @@ func (pol *Policy) compileACLWithAutogroupSelf(
if len(autogroupSelfDests) > 0 && !node.IsTagged() {
// Pre-filter to same-user untagged devices once - reuse for both sources and destinations
sameUserNodes := make([]types.NodeView, 0)
for _, n := range nodes.All() {
if !n.IsTagged() && n.User().ID() == node.User().ID() {
sameUserNodes = append(sameUserNodes, n)
@ -197,6 +207,7 @@ func (pol *Policy) compileACLWithAutogroupSelf(
if len(sameUserNodes) > 0 {
// Filter sources to only same-user untagged devices
var srcIPs netipx.IPSetBuilder
for _, ips := range resolvedSrcIPs {
for _, n := range sameUserNodes {
// Check if any of this node's IPs are in the source set
@ -213,6 +224,7 @@ func (pol *Policy) compileACLWithAutogroupSelf(
if srcSet != nil && len(srcSet.Prefixes()) > 0 {
var destPorts []tailcfg.NetPortRange
for _, dest := range autogroupSelfDests {
for _, n := range sameUserNodes {
for _, port := range dest.Ports {
@ -318,13 +330,14 @@ func sshAction(accept bool, duration time.Duration) tailcfg.SSHAction {
}
}
//nolint:gocyclo // complex SSH policy compilation logic
func (pol *Policy) compileSSHPolicy(
users types.Users,
node types.NodeView,
nodes views.Slice[types.NodeView],
) (*tailcfg.SSHPolicy, error) {
if pol == nil || pol.SSHs == nil || len(pol.SSHs) == 0 {
return nil, nil
return nil, nil //nolint:nilnil // intentional: no SSH policy when none configured
}
log.Trace().Caller().Msgf("compiling SSH policy for node %q", node.Hostname())
@ -335,8 +348,10 @@ func (pol *Policy) compileSSHPolicy(
// Separate destinations into autogroup:self and others
// This is needed because autogroup:self requires filtering sources to same-user only,
// while other destinations should use all resolved sources
var autogroupSelfDests []Alias
var otherDests []Alias
var (
autogroupSelfDests []Alias
otherDests []Alias
)
for _, dst := range rule.Destinations {
if ag, ok := dst.(*AutoGroup); ok && ag.Is(AutoGroupSelf) {
@ -359,6 +374,7 @@ func (pol *Policy) compileSSHPolicy(
}
var action tailcfg.SSHAction
switch rule.Action {
case SSHActionAccept:
action = sshAction(true, 0)
@ -374,9 +390,11 @@ func (pol *Policy) compileSSHPolicy(
// by default, we do not allow root unless explicitly stated
userMap["root"] = ""
}
if rule.Users.ContainsRoot() {
userMap["root"] = "root"
}
for _, u := range rule.Users.NormalUsers() {
userMap[u.String()] = u.String()
}
@ -386,6 +404,7 @@ func (pol *Policy) compileSSHPolicy(
if len(autogroupSelfDests) > 0 && !node.IsTagged() {
// Build destination set for autogroup:self (same-user untagged devices only)
var dest netipx.IPSetBuilder
for _, n := range nodes.All() {
if !n.IsTagged() && n.User().ID() == node.User().ID() {
n.AppendToIPSet(&dest)
@ -402,6 +421,7 @@ func (pol *Policy) compileSSHPolicy(
// Filter sources to only same-user untagged devices
// Pre-filter to same-user untagged devices for efficiency
sameUserNodes := make([]types.NodeView, 0)
for _, n := range nodes.All() {
if !n.IsTagged() && n.User().ID() == node.User().ID() {
sameUserNodes = append(sameUserNodes, n)
@ -409,6 +429,7 @@ func (pol *Policy) compileSSHPolicy(
}
var filteredSrcIPs netipx.IPSetBuilder
for _, n := range sameUserNodes {
// Check if any of this node's IPs are in the source set
if slices.ContainsFunc(n.IPs(), srcIPs.Contains) {
@ -444,11 +465,13 @@ func (pol *Policy) compileSSHPolicy(
if len(otherDests) > 0 {
// Build destination set for other destinations
var dest netipx.IPSetBuilder
for _, dst := range otherDests {
ips, err := dst.Resolve(pol, users, nodes)
if err != nil {
log.Trace().Caller().Err(err).Msgf("resolving destination ips")
}
if ips != nil {
dest.AddSet(ips)
}

View file

@ -623,7 +623,9 @@ func TestCompileSSHPolicy_UserMapping(t *testing.T) {
if sshPolicy == nil {
return // Expected empty result
}
assert.Empty(t, sshPolicy.Rules, "SSH policy should be empty when no rules match")
return
}
@ -709,7 +711,7 @@ func TestCompileSSHPolicy_CheckAction(t *testing.T) {
}
// TestSSHIntegrationReproduction reproduces the exact scenario from the integration test
// TestSSHOneUserToAll that was failing with empty sshUsers
// TestSSHOneUserToAll that was failing with empty sshUsers.
func TestSSHIntegrationReproduction(t *testing.T) {
// Create users matching the integration test
users := types.Users{
@ -775,7 +777,7 @@ func TestSSHIntegrationReproduction(t *testing.T) {
}
// TestSSHJSONSerialization verifies that the SSH policy can be properly serialized
// to JSON and that the sshUsers field is not empty
// to JSON and that the sshUsers field is not empty.
func TestSSHJSONSerialization(t *testing.T) {
users := types.Users{
{Name: "user1", Model: gorm.Model{ID: 1}},
@ -815,6 +817,7 @@ func TestSSHJSONSerialization(t *testing.T) {
// Parse back to verify structure
var parsed tailcfg.SSHPolicy
err = json.Unmarshal(jsonData, &parsed)
require.NoError(t, err)
@ -899,6 +902,7 @@ func TestCompileFilterRulesForNodeWithAutogroupSelf(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(rules) != 1 {
t.Fatalf("expected 1 rule, got %d", len(rules))
}
@ -915,6 +919,7 @@ func TestCompileFilterRulesForNodeWithAutogroupSelf(t *testing.T) {
found := false
addr := netip.MustParseAddr(expectedIP)
for _, prefix := range rule.SrcIPs {
pref := netip.MustParsePrefix(prefix)
if pref.Contains(addr) {
@ -932,6 +937,7 @@ func TestCompileFilterRulesForNodeWithAutogroupSelf(t *testing.T) {
excludedSourceIPs := []string{"100.64.0.3", "100.64.0.4", "100.64.0.5", "100.64.0.6"}
for _, excludedIP := range excludedSourceIPs {
addr := netip.MustParseAddr(excludedIP)
for _, prefix := range rule.SrcIPs {
pref := netip.MustParsePrefix(prefix)
if pref.Contains(addr) {
@ -1144,7 +1150,8 @@ func TestAutogroupTagged(t *testing.T) {
require.NoError(t, err)
// Verify autogroup:tagged includes all tagged nodes
taggedIPs, err := AutoGroupTagged.Resolve(policy, users, nodes.ViewSlice())
ag := AutoGroupTagged
taggedIPs, err := ag.Resolve(policy, users, nodes.ViewSlice())
require.NoError(t, err)
require.NotNil(t, taggedIPs)
@ -1366,14 +1373,14 @@ func TestAutogroupSelfWithGroupSource(t *testing.T) {
assert.Empty(t, rules3, "user3 should have no rules")
}
// Helper function to create IP addresses for testing
// Helper function to create IP addresses for testing.
func createAddr(ip string) *netip.Addr {
addr, _ := netip.ParseAddr(ip)
return &addr
}
// TestSSHWithAutogroupSelfInDestination verifies that SSH policies work correctly
// with autogroup:self in destinations
// with autogroup:self in destinations.
func TestSSHWithAutogroupSelfInDestination(t *testing.T) {
users := types.Users{
{Model: gorm.Model{ID: 1}, Name: "user1"},
@ -1421,6 +1428,7 @@ func TestSSHWithAutogroupSelfInDestination(t *testing.T) {
for i, p := range rule.Principals {
principalIPs[i] = p.NodeIP
}
assert.ElementsMatch(t, []string{"100.64.0.1", "100.64.0.2"}, principalIPs)
// Test for user2's first node
@ -1439,12 +1447,14 @@ func TestSSHWithAutogroupSelfInDestination(t *testing.T) {
for i, p := range rule2.Principals {
principalIPs2[i] = p.NodeIP
}
assert.ElementsMatch(t, []string{"100.64.0.3", "100.64.0.4"}, principalIPs2)
// Test for tagged node (should have no SSH rules)
node5 := nodes[4].View()
sshPolicy3, err := policy.compileSSHPolicy(users, node5, nodes.ViewSlice())
require.NoError(t, err)
if sshPolicy3 != nil {
assert.Empty(t, sshPolicy3.Rules, "tagged nodes should not get SSH rules with autogroup:self")
}
@ -1452,7 +1462,7 @@ func TestSSHWithAutogroupSelfInDestination(t *testing.T) {
// TestSSHWithAutogroupSelfAndSpecificUser verifies that when a specific user
// is in the source and autogroup:self in destination, only that user's devices
// can SSH (and only if they match the target user)
// can SSH (and only if they match the target user).
func TestSSHWithAutogroupSelfAndSpecificUser(t *testing.T) {
users := types.Users{
{Model: gorm.Model{ID: 1}, Name: "user1"},
@ -1494,18 +1504,20 @@ func TestSSHWithAutogroupSelfAndSpecificUser(t *testing.T) {
for i, p := range rule.Principals {
principalIPs[i] = p.NodeIP
}
assert.ElementsMatch(t, []string{"100.64.0.1", "100.64.0.2"}, principalIPs)
// For user2's node: should have no rules (user1's devices can't match user2's self)
node3 := nodes[2].View()
sshPolicy2, err := policy.compileSSHPolicy(users, node3, nodes.ViewSlice())
require.NoError(t, err)
if sshPolicy2 != nil {
assert.Empty(t, sshPolicy2.Rules, "user2 should have no SSH rules since source is user1")
}
}
// TestSSHWithAutogroupSelfAndGroup verifies SSH with group sources and autogroup:self destinations
// TestSSHWithAutogroupSelfAndGroup verifies SSH with group sources and autogroup:self destinations.
func TestSSHWithAutogroupSelfAndGroup(t *testing.T) {
users := types.Users{
{Model: gorm.Model{ID: 1}, Name: "user1"},
@ -1552,19 +1564,21 @@ func TestSSHWithAutogroupSelfAndGroup(t *testing.T) {
for i, p := range rule.Principals {
principalIPs[i] = p.NodeIP
}
assert.ElementsMatch(t, []string{"100.64.0.1", "100.64.0.2"}, principalIPs)
// For user3's node: should have no rules (not in group:admins)
node5 := nodes[4].View()
sshPolicy2, err := policy.compileSSHPolicy(users, node5, nodes.ViewSlice())
require.NoError(t, err)
if sshPolicy2 != nil {
assert.Empty(t, sshPolicy2.Rules, "user3 should have no SSH rules (not in group)")
}
}
// TestSSHWithAutogroupSelfExcludesTaggedDevices verifies that tagged devices
// are excluded from both sources and destinations when autogroup:self is used
// are excluded from both sources and destinations when autogroup:self is used.
func TestSSHWithAutogroupSelfExcludesTaggedDevices(t *testing.T) {
users := types.Users{
{Model: gorm.Model{ID: 1}, Name: "user1"},
@ -1609,6 +1623,7 @@ func TestSSHWithAutogroupSelfExcludesTaggedDevices(t *testing.T) {
for i, p := range rule.Principals {
principalIPs[i] = p.NodeIP
}
assert.ElementsMatch(t, []string{"100.64.0.1", "100.64.0.2"}, principalIPs,
"should only include untagged devices")
@ -1616,6 +1631,7 @@ func TestSSHWithAutogroupSelfExcludesTaggedDevices(t *testing.T) {
node3 := nodes[2].View()
sshPolicy2, err := policy.compileSSHPolicy(users, node3, nodes.ViewSlice())
require.NoError(t, err)
if sshPolicy2 != nil {
assert.Empty(t, sshPolicy2.Rules, "tagged node should get no SSH rules with autogroup:self")
}
@ -1664,10 +1680,12 @@ func TestSSHWithAutogroupSelfAndMixedDestinations(t *testing.T) {
// Verify autogroup:self rule has filtered sources (only same-user devices)
selfRule := sshPolicy1.Rules[0]
require.Len(t, selfRule.Principals, 2, "autogroup:self rule should only have user1's devices")
selfPrincipals := make([]string, len(selfRule.Principals))
for i, p := range selfRule.Principals {
selfPrincipals[i] = p.NodeIP
}
require.ElementsMatch(t, []string{"100.64.0.1", "100.64.0.2"}, selfPrincipals,
"autogroup:self rule should only include same-user untagged devices")
@ -1679,10 +1697,12 @@ func TestSSHWithAutogroupSelfAndMixedDestinations(t *testing.T) {
require.Len(t, sshPolicyRouter.Rules, 1, "router should have 1 SSH rule (tag:router)")
routerRule := sshPolicyRouter.Rules[0]
routerPrincipals := make([]string, len(routerRule.Principals))
for i, p := range routerRule.Principals {
routerPrincipals[i] = p.NodeIP
}
require.Contains(t, routerPrincipals, "100.64.0.1", "router rule should include user1's device (unfiltered sources)")
require.Contains(t, routerPrincipals, "100.64.0.2", "router rule should include user1's other device (unfiltered sources)")
require.Contains(t, routerPrincipals, "100.64.0.3", "router rule should include user2's device (unfiltered sources)")

View file

@ -111,6 +111,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
Filter: filter,
Policy: pm.pol,
})
filterChanged := filterHash != pm.filterHash
if filterChanged {
log.Debug().
@ -120,7 +121,9 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
Int("filter.rules.new", len(filter)).
Msg("Policy filter hash changed")
}
pm.filter = filter
pm.filterHash = filterHash
if filterChanged {
pm.matchers = matcher.MatchesFromFilterRules(pm.filter)
@ -135,6 +138,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
}
tagOwnerMapHash := deephash.Hash(&tagMap)
tagOwnerChanged := tagOwnerMapHash != pm.tagOwnerMapHash
if tagOwnerChanged {
log.Debug().
@ -144,6 +148,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
Int("tagOwners.new", len(tagMap)).
Msg("Tag owner hash changed")
}
pm.tagOwnerMap = tagMap
pm.tagOwnerMapHash = tagOwnerMapHash
@ -153,6 +158,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
}
autoApproveMapHash := deephash.Hash(&autoMap)
autoApproveChanged := autoApproveMapHash != pm.autoApproveMapHash
if autoApproveChanged {
log.Debug().
@ -162,10 +168,12 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
Int("autoApprovers.new", len(autoMap)).
Msg("Auto-approvers hash changed")
}
pm.autoApproveMap = autoMap
pm.autoApproveMapHash = autoApproveMapHash
exitSetHash := deephash.Hash(&exitSet)
exitSetChanged := exitSetHash != pm.exitSetHash
if exitSetChanged {
log.Debug().
@ -173,6 +181,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
Str("exitSet.hash.new", exitSetHash.String()[:8]).
Msg("Exit node set hash changed")
}
pm.exitSet = exitSet
pm.exitSetHash = exitSetHash
@ -199,6 +208,7 @@ func (pm *PolicyManager) updateLocked() (bool, error) {
if !needsUpdate {
log.Trace().
Msg("Policy evaluation detected no changes - all hashes match")
return false, nil
}
@ -224,6 +234,7 @@ func (pm *PolicyManager) SSHPolicy(node types.NodeView) (*tailcfg.SSHPolicy, err
if err != nil {
return nil, fmt.Errorf("compiling SSH policy: %w", err)
}
pm.sshPolicyMap[node.ID()] = sshPol
return sshPol, nil
@ -403,6 +414,7 @@ func (pm *PolicyManager) filterForNodeLocked(node types.NodeView) ([]tailcfg.Fil
reducedFilter := policyutil.ReduceFilterRules(node, pm.filter)
pm.filterRulesMap[node.ID()] = reducedFilter
return reducedFilter, nil
}
@ -447,7 +459,7 @@ func (pm *PolicyManager) FilterForNode(node types.NodeView) ([]tailcfg.FilterRul
// This is different from FilterForNode which returns REDUCED rules for packet filtering.
//
// For global policies: returns the global matchers (same for all nodes)
// For autogroup:self: returns node-specific matchers from unreduced compiled rules
// For autogroup:self: returns node-specific matchers from unreduced compiled rules.
func (pm *PolicyManager) MatchersForNode(node types.NodeView) ([]matcher.Match, error) {
if pm == nil {
return nil, nil
@ -479,6 +491,7 @@ func (pm *PolicyManager) SetUsers(users []types.User) (bool, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.users = users
// Clear SSH policy map when users change to force SSH policy recomputation
@ -690,6 +703,7 @@ func (pm *PolicyManager) NodeCanApproveRoute(node types.NodeView, route netip.Pr
if pm.exitSet == nil {
return false
}
if slices.ContainsFunc(node.IPs(), pm.exitSet.Contains) {
return true
}
@ -753,8 +767,10 @@ func (pm *PolicyManager) DebugString() string {
}
fmt.Fprintf(&sb, "AutoApprover (%d):\n", len(pm.autoApproveMap))
for prefix, approveAddrs := range pm.autoApproveMap {
fmt.Fprintf(&sb, "\t%s:\n", prefix)
for _, iprange := range approveAddrs.Ranges() {
fmt.Fprintf(&sb, "\t\t%s\n", iprange)
}
@ -763,14 +779,17 @@ func (pm *PolicyManager) DebugString() string {
sb.WriteString("\n\n")
fmt.Fprintf(&sb, "TagOwner (%d):\n", len(pm.tagOwnerMap))
for prefix, tagOwners := range pm.tagOwnerMap {
fmt.Fprintf(&sb, "\t%s:\n", prefix)
for _, iprange := range tagOwners.Ranges() {
fmt.Fprintf(&sb, "\t\t%s\n", iprange)
}
}
sb.WriteString("\n\n")
if pm.filter != nil {
filter, err := json.MarshalIndent(pm.filter, "", " ")
if err == nil {
@ -783,6 +802,7 @@ func (pm *PolicyManager) DebugString() string {
sb.WriteString("\n\n")
sb.WriteString("Matchers:\n")
sb.WriteString("an internal structure used to filter nodes and routes\n")
for _, match := range pm.matchers {
sb.WriteString(match.DebugString())
sb.WriteString("\n")
@ -790,6 +810,7 @@ func (pm *PolicyManager) DebugString() string {
sb.WriteString("\n\n")
sb.WriteString("Nodes:\n")
for _, node := range pm.nodes.All() {
sb.WriteString(node.String())
sb.WriteString("\n")
@ -867,6 +888,7 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
// Check if IPs changed (simple check - could be more sophisticated)
oldIPs := oldNode.IPs()
newIPs := newNode.IPs()
if len(oldIPs) != len(newIPs) {
affectedUsers[newNode.User().ID()] = struct{}{}
@ -888,6 +910,7 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
for nodeID := range pm.filterRulesMap {
// Find the user for this cached node
var nodeUserID uint
found := false
// Check in new nodes first
@ -899,8 +922,10 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
found = true
break
}
nodeUserID = node.User().ID()
found = true
break
}
}
@ -913,8 +938,10 @@ func (pm *PolicyManager) invalidateAutogroupSelfCache(oldNodes, newNodes views.S
found = true
break
}
nodeUserID = node.User().ID()
found = true
break
}
}

View file

@ -14,7 +14,7 @@ import (
"tailscale.com/types/ptr"
)
func node(name, ipv4, ipv6 string, user types.User, hostinfo *tailcfg.Hostinfo) *types.Node {
func node(name, ipv4, ipv6 string, user types.User) *types.Node {
return &types.Node{
ID: 0,
Hostname: name,
@ -22,7 +22,6 @@ func node(name, ipv4, ipv6 string, user types.User, hostinfo *tailcfg.Hostinfo)
IPv6: ap(ipv6),
User: ptr.To(user),
UserID: ptr.To(user.ID),
Hostinfo: hostinfo,
}
}
@ -57,6 +56,7 @@ func TestPolicyManager(t *testing.T) {
if diff := cmp.Diff(tt.wantFilter, filter); diff != "" {
t.Errorf("Filter() filter mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(
tt.wantMatchers,
matchers,
@ -77,6 +77,7 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
{Model: gorm.Model{ID: 3}, Name: "user3", Email: "user3@headscale.net"},
}
//nolint:goconst // test-specific inline policy for clarity
policy := `{
"acls": [
{
@ -88,14 +89,14 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
}`
initialNodes := types.Nodes{
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0], nil),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0], nil),
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1], nil),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2], nil),
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0]),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0]),
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1]),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2]),
}
for i, n := range initialNodes {
n.ID = types.NodeID(i + 1)
n.ID = types.NodeID(i + 1) //nolint:gosec // safe conversion in test
}
pm, err := NewPolicyManager([]byte(policy), users, initialNodes.ViewSlice())
@ -107,7 +108,7 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
require.NoError(t, err)
}
require.Equal(t, len(initialNodes), len(pm.filterRulesMap))
require.Len(t, pm.filterRulesMap, len(initialNodes))
tests := []struct {
name string
@ -118,10 +119,10 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
{
name: "no_changes",
newNodes: types.Nodes{
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0], nil),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0], nil),
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1], nil),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2], nil),
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0]),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0]),
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1]),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2]),
},
expectedCleared: 0,
description: "No changes should clear no cache entries",
@ -129,11 +130,11 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
{
name: "node_added",
newNodes: types.Nodes{
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0], nil),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0], nil),
node("user1-node3", "100.64.0.5", "fd7a:115c:a1e0::5", users[0], nil), // New node
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1], nil),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2], nil),
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0]),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0]),
node("user1-node3", "100.64.0.5", "fd7a:115c:a1e0::5", users[0]), // New node
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1]),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2]),
},
expectedCleared: 2, // user1's existing nodes should be cleared
description: "Adding a node should clear cache for that user's existing nodes",
@ -141,10 +142,10 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
{
name: "node_removed",
newNodes: types.Nodes{
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0], nil),
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0]),
// user1-node2 removed
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1], nil),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2], nil),
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1]),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2]),
},
expectedCleared: 2, // user1's remaining node + removed node should be cleared
description: "Removing a node should clear cache for that user's remaining nodes",
@ -152,10 +153,10 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
{
name: "user_changed",
newNodes: types.Nodes{
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0], nil),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[2], nil), // Changed to user3
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1], nil),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2], nil),
node("user1-node1", "100.64.0.1", "fd7a:115c:a1e0::1", users[0]),
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[2]), // Changed to user3
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1]),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2]),
},
expectedCleared: 3, // user1's node + user2's node + user3's nodes should be cleared
description: "Changing a node's user should clear cache for both old and new users",
@ -163,10 +164,10 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
{
name: "ip_changed",
newNodes: types.Nodes{
node("user1-node1", "100.64.0.10", "fd7a:115c:a1e0::10", users[0], nil), // IP changed
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0], nil),
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1], nil),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2], nil),
node("user1-node1", "100.64.0.10", "fd7a:115c:a1e0::10", users[0]), // IP changed
node("user1-node2", "100.64.0.2", "fd7a:115c:a1e0::2", users[0]),
node("user2-node1", "100.64.0.3", "fd7a:115c:a1e0::3", users[1]),
node("user3-node1", "100.64.0.4", "fd7a:115c:a1e0::4", users[2]),
},
expectedCleared: 2, // user1's nodes should be cleared
description: "Changing a node's IP should clear cache for that user's nodes",
@ -177,15 +178,18 @@ func TestInvalidateAutogroupSelfCache(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
for i, n := range tt.newNodes {
found := false
for _, origNode := range initialNodes {
if n.Hostname == origNode.Hostname {
n.ID = origNode.ID
found = true
break
}
}
if !found {
n.ID = types.NodeID(len(initialNodes) + i + 1)
n.ID = types.NodeID(len(initialNodes) + i + 1) //nolint:gosec // safe conversion in test
}
}
@ -370,16 +374,16 @@ func TestInvalidateGlobalPolicyCache(t *testing.T) {
// TestAutogroupSelfReducedVsUnreducedRules verifies that:
// 1. BuildPeerMap uses unreduced compiled rules for determining peer relationships
// 2. FilterForNode returns reduced compiled rules for packet filters
// 2. FilterForNode returns reduced compiled rules for packet filters.
func TestAutogroupSelfReducedVsUnreducedRules(t *testing.T) {
user1 := types.User{Model: gorm.Model{ID: 1}, Name: "user1", Email: "user1@headscale.net"}
user2 := types.User{Model: gorm.Model{ID: 2}, Name: "user2", Email: "user2@headscale.net"}
users := types.Users{user1, user2}
// Create two nodes
node1 := node("node1", "100.64.0.1", "fd7a:115c:a1e0::1", user1, nil)
node1 := node("node1", "100.64.0.1", "fd7a:115c:a1e0::1", user1)
node1.ID = 1
node2 := node("node2", "100.64.0.2", "fd7a:115c:a1e0::2", user2, nil)
node2 := node("node2", "100.64.0.2", "fd7a:115c:a1e0::2", user2)
node2.ID = 2
nodes := types.Nodes{node1, node2}
@ -410,6 +414,7 @@ func TestAutogroupSelfReducedVsUnreducedRules(t *testing.T) {
// FilterForNode should return reduced rules - verify they only contain the node's own IPs as destinations
// For node1, destinations should only be node1's IPs
node1IPs := []string{"100.64.0.1/32", "100.64.0.1", "fd7a:115c:a1e0::1/128", "fd7a:115c:a1e0::1"}
for _, rule := range filterNode1 {
for _, dst := range rule.DstPorts {
require.Contains(t, node1IPs, dst.IP,
@ -419,6 +424,7 @@ func TestAutogroupSelfReducedVsUnreducedRules(t *testing.T) {
// For node2, destinations should only be node2's IPs
node2IPs := []string{"100.64.0.2/32", "100.64.0.2", "fd7a:115c:a1e0::2/128", "fd7a:115c:a1e0::2"}
for _, rule := range filterNode2 {
for _, dst := range rule.DstPorts {
require.Contains(t, node2IPs, dst.IP,

View file

@ -9655,7 +9655,7 @@ func TestTailscaleCompatErrorCases(t *testing.T) {
{"action": "accept", "src": ["tag:nonexistent"], "dst": ["tag:server:22"]}
]
}`,
wantErr: `Tag "tag:nonexistent" is not defined in the Policy`,
wantErr: `tag not defined in policy: "tag:nonexistent"`,
reference: "Test 6.4: tag:nonexistent → tag:server:22",
},
@ -9674,7 +9674,7 @@ func TestTailscaleCompatErrorCases(t *testing.T) {
{"action": "accept", "src": ["autogroup:self"], "dst": ["tag:server:22"]}
]
}`,
wantErr: `"autogroup:self" used in source, it can only be used in ACL destinations`,
wantErr: `autogroup:self can only be used in ACL destinations`,
reference: "Test 13.41: autogroup:self as SOURCE",
},

File diff suppressed because it is too large Load diff

View file

@ -82,6 +82,7 @@ func TestMarshalJSON(t *testing.T) {
// Unmarshal back to verify round trip
var roundTripped Policy
err = json.Unmarshal(marshalled, &roundTripped)
require.NoError(t, err)
@ -366,7 +367,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: "alias v2.Asterix is not supported for SSH source",
wantErr: "alias not supported for SSH source: v2.Asterix",
},
{
name: "invalid-username",
@ -393,7 +394,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `group must start with "group:", got: "grou:example"`,
wantErr: `group must start with 'group:', got: "grou:example"`,
},
{
name: "group-in-group",
@ -408,7 +409,7 @@ func TestUnmarshalPolicy(t *testing.T) {
}
`,
// wantErr: `username must contain @, got: "group:inner"`,
wantErr: `nested groups are not allowed, found "group:inner" inside "group:example"`,
wantErr: `nested groups are not allowed: found "group:inner" inside "group:example"`,
},
{
name: "invalid-addr",
@ -419,7 +420,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `hostname "derp" contains an invalid IP address: "10.0"`,
wantErr: `hostname contains invalid IP address: hostname "derp" address "10.0"`,
},
{
name: "invalid-prefix",
@ -430,7 +431,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `hostname "derp" contains an invalid IP address: "10.0/42"`,
wantErr: `hostname contains invalid IP address: hostname "derp" address "10.0/42"`,
},
// TODO(kradalby): Figure out why this doesn't work.
// {
@ -459,7 +460,7 @@ func TestUnmarshalPolicy(t *testing.T) {
],
}
`,
wantErr: `autogroup is invalid, got: "autogroup:invalid", must be one of [autogroup:internet autogroup:member autogroup:nonroot autogroup:tagged autogroup:self]`,
wantErr: `invalid autogroup: got "autogroup:invalid", must be one of [autogroup:internet autogroup:member autogroup:nonroot autogroup:tagged autogroup:self]`,
},
{
name: "undefined-hostname-errors-2490",
@ -478,7 +479,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `host "user1" is not defined in the policy, please define or remove the reference to it`,
wantErr: `host not defined in policy: "user1"`,
},
{
name: "defined-hostname-does-not-err-2490",
@ -571,7 +572,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `"autogroup:internet" used in source, it can only be used in ACL destinations`,
wantErr: `autogroup:internet can only be used in ACL destinations`,
},
{
name: "autogroup:internet-in-ssh-src-not-allowed",
@ -590,7 +591,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `"autogroup:internet" used in SSH source, it can only be used in ACL destinations`,
wantErr: `tag not defined in policy: "tag:test"`,
},
{
name: "autogroup:internet-in-ssh-dst-not-allowed",
@ -609,7 +610,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `"autogroup:internet" used in SSH destination, it can only be used in ACL destinations`,
wantErr: `autogroup:internet can only be used in ACL destinations`,
},
{
name: "ssh-basic",
@ -762,7 +763,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
wantErr: `group not defined in policy: "group:notdefined"`,
},
{
name: "group-must-be-defined-acl-dst",
@ -781,7 +782,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
wantErr: `group not defined in policy: "group:notdefined"`,
},
{
name: "group-must-be-defined-acl-ssh-src",
@ -800,7 +801,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
wantErr: `user destination requires source to contain only that same user "user@"`,
},
{
name: "group-must-be-defined-acl-tagOwner",
@ -811,7 +812,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
wantErr: `group not defined in policy: "group:notdefined"`,
},
{
name: "group-must-be-defined-acl-autoapprover-route",
@ -824,7 +825,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
wantErr: `group not defined in policy: "group:notdefined"`,
},
{
name: "group-must-be-defined-acl-autoapprover-exitnode",
@ -835,7 +836,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `Group "group:notdefined" is not defined in the Policy, please define or remove the reference to it`,
wantErr: `group not defined in policy: "group:notdefined"`,
},
{
name: "tag-must-be-defined-acl-src",
@ -854,7 +855,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag "tag:notdefined" is not defined in the policy, please define or remove the reference to it`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-dst",
@ -873,7 +874,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag "tag:notdefined" is not defined in the policy, please define or remove the reference to it`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-ssh-src",
@ -892,7 +893,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag "tag:notdefined" is not defined in the policy, please define or remove the reference to it`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-ssh-dst",
@ -914,7 +915,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `tag "tag:notdefined" is not defined in the policy, please define or remove the reference to it`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-autoapprover-route",
@ -927,7 +928,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `tag "tag:notdefined" is not defined in the policy, please define or remove the reference to it`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
},
{
name: "tag-must-be-defined-acl-autoapprover-exitnode",
@ -938,7 +939,7 @@ func TestUnmarshalPolicy(t *testing.T) {
},
}
`,
wantErr: `tag "tag:notdefined" is not defined in the policy, please define or remove the reference to it`,
wantErr: `tag not defined in policy: "tag:notdefined"`,
},
{
name: "missing-dst-port-is-err",
@ -957,7 +958,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `hostport must contain a colon (":")`,
wantErr: `hostport must contain a colon`,
},
{
name: "dst-port-zero-is-err",
@ -987,7 +988,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `unknown field "rules"`,
wantErr: `unknown field: "rules"`,
},
{
name: "disallow-unsupported-fields-nested",
@ -1010,7 +1011,7 @@ func TestUnmarshalPolicy(t *testing.T) {
}
}
`,
wantErr: `group must start with "group:", got: "INVALID_GROUP_FIELD"`,
wantErr: `group must start with 'group:', got: "INVALID_GROUP_FIELD"`,
},
{
name: "invalid-group-datatype",
@ -1022,7 +1023,7 @@ func TestUnmarshalPolicy(t *testing.T) {
}
}
`,
wantErr: `group "group:invalid" value must be an array of users, got string: "should fail"`,
wantErr: `group value must be an array of users: group "group:invalid" got string: "should fail"`,
},
{
name: "invalid-group-name-and-datatype-fails-on-name-first",
@ -1034,7 +1035,7 @@ func TestUnmarshalPolicy(t *testing.T) {
}
}
`,
wantErr: `group must start with "group:", got: "INVALID_GROUP_FIELD"`,
wantErr: `group must start with 'group:', got: "INVALID_GROUP_FIELD"`,
},
{
name: "disallow-unsupported-fields-hosts-level",
@ -1046,7 +1047,7 @@ func TestUnmarshalPolicy(t *testing.T) {
}
}
`,
wantErr: `hostname "INVALID_HOST_FIELD" contains an invalid IP address: "should fail"`,
wantErr: `hostname contains invalid IP address: hostname "INVALID_HOST_FIELD" address "should fail"`,
},
{
name: "disallow-unsupported-fields-tagowners-level",
@ -1058,7 +1059,7 @@ func TestUnmarshalPolicy(t *testing.T) {
}
}
`,
wantErr: `tag has to start with "tag:", got: "INVALID_TAG_FIELD"`,
wantErr: `tag must start with 'tag:', got: "INVALID_TAG_FIELD"`,
},
{
name: "disallow-unsupported-fields-acls-level",
@ -1075,7 +1076,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `unknown field "INVALID_ACL_FIELD"`,
wantErr: `unknown field: "INVALID_ACL_FIELD"`,
},
{
name: "disallow-unsupported-fields-ssh-level",
@ -1092,7 +1093,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `unknown field "INVALID_SSH_FIELD"`,
wantErr: `unknown field: "INVALID_SSH_FIELD"`,
},
{
name: "disallow-unsupported-fields-policy-level",
@ -1109,7 +1110,7 @@ func TestUnmarshalPolicy(t *testing.T) {
"INVALID_POLICY_FIELD": "should fail at policy level"
}
`,
wantErr: `unknown field "INVALID_POLICY_FIELD"`,
wantErr: `unknown field: "INVALID_POLICY_FIELD"`,
},
{
name: "disallow-unsupported-fields-autoapprovers-level",
@ -1124,7 +1125,7 @@ func TestUnmarshalPolicy(t *testing.T) {
}
}
`,
wantErr: `unknown field "INVALID_AUTO_APPROVER_FIELD"`,
wantErr: `unknown field: "INVALID_AUTO_APPROVER_FIELD"`,
},
// headscale-admin uses # in some field names to add metadata, so we will ignore
// those to ensure it doesnt break.
@ -1183,7 +1184,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `unknown field "proto"`,
wantErr: `unknown field: "proto"`,
},
{
name: "protocol-wildcard-not-allowed",
@ -1279,7 +1280,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `leading 0 not permitted in protocol number "0"`,
wantErr: `leading 0 not permitted in protocol number: "0"`,
},
{
name: "protocol-empty-applies-to-tcp-udp-only",
@ -1326,7 +1327,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `protocol "icmp" does not support specific ports; only "*" is allowed`,
wantErr: `protocol does not support specific ports: "icmp", only "*" is allowed`,
},
{
name: "protocol-icmp-with-wildcard-port-allowed",
@ -1374,7 +1375,7 @@ func TestUnmarshalPolicy(t *testing.T) {
]
}
`,
wantErr: `protocol "gre" does not support specific ports; only "*" is allowed`,
wantErr: `protocol does not support specific ports: "gre", only "*" is allowed`,
},
{
name: "protocol-tcp-with-specific-port-allowed",
@ -2081,7 +2082,7 @@ func TestResolvePolicy(t *testing.T) {
IPv4: ap("100.100.101.103"),
},
},
wantErr: `user with token "invaliduser@" not found`,
wantErr: `user not found: token "invaliduser@"`,
},
{
name: "invalid-tag",
@ -2105,7 +2106,7 @@ func TestResolvePolicy(t *testing.T) {
},
{
name: "autogroup-member-comprehensive",
toResolve: ptr.To(AutoGroup(AutoGroupMember)),
toResolve: ptr.To(AutoGroupMember),
nodes: types.Nodes{
// Node with no tags (should be included - is a member)
{
@ -2155,7 +2156,7 @@ func TestResolvePolicy(t *testing.T) {
},
{
name: "autogroup-tagged",
toResolve: ptr.To(AutoGroup(AutoGroupTagged)),
toResolve: ptr.To(AutoGroupTagged),
nodes: types.Nodes{
// Node with no tags (should be excluded - not tagged)
{
@ -2266,6 +2267,7 @@ func TestResolvePolicy(t *testing.T) {
}
var prefs []netip.Prefix
if ips != nil {
if p := ips.Prefixes(); len(p) > 0 {
prefs = p
@ -2437,9 +2439,11 @@ func TestResolveAutoApprovers(t *testing.T) {
t.Errorf("resolveAutoApprovers() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.want, got, cmps...); diff != "" {
t.Errorf("resolveAutoApprovers() mismatch (-want +got):\n%s", diff)
}
if tt.wantAllIPRoutes != nil {
if gotAllIPRoutes == nil {
t.Error("resolveAutoApprovers() expected non-nil allIPRoutes, got nil")
@ -2586,6 +2590,7 @@ func mustIPSet(prefixes ...string) *netipx.IPSet {
for _, p := range prefixes {
builder.AddPrefix(mp(p))
}
ipSet, _ := builder.IPSet()
return ipSet
@ -2595,6 +2600,7 @@ func ipSetComparer(x, y *netipx.IPSet) bool {
if x == nil || y == nil {
return x == y
}
return cmp.Equal(x.Prefixes(), y.Prefixes(), util.Comparers...)
}
@ -2823,6 +2829,7 @@ func TestResolveTagOwners(t *testing.T) {
t.Errorf("resolveTagOwners() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.want, got, cmps...); diff != "" {
t.Errorf("resolveTagOwners() mismatch (-want +got):\n%s", diff)
}
@ -3098,6 +3105,7 @@ func TestNodeCanHaveTag(t *testing.T) {
require.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
got := pm.NodeCanHaveTag(tt.node.View(), tt.tag)
@ -3358,6 +3366,7 @@ func TestACL_UnmarshalJSON_WithCommentFields(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var acl ACL
err := json.Unmarshal([]byte(tt.input), &acl)
if tt.wantErr {
@ -3368,8 +3377,8 @@ func TestACL_UnmarshalJSON_WithCommentFields(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, tt.expected.Action, acl.Action)
assert.Equal(t, tt.expected.Protocol, acl.Protocol)
assert.Equal(t, len(tt.expected.Sources), len(acl.Sources))
assert.Equal(t, len(tt.expected.Destinations), len(acl.Destinations))
assert.Len(t, acl.Sources, len(tt.expected.Sources))
assert.Len(t, acl.Destinations, len(tt.expected.Destinations))
// Compare sources
for i, expectedSrc := range tt.expected.Sources {
@ -3409,14 +3418,15 @@ func TestACL_UnmarshalJSON_Roundtrip(t *testing.T) {
// Unmarshal back
var unmarshaled ACL
err = json.Unmarshal(jsonBytes, &unmarshaled)
require.NoError(t, err)
// Should be equal
assert.Equal(t, original.Action, unmarshaled.Action)
assert.Equal(t, original.Protocol, unmarshaled.Protocol)
assert.Equal(t, len(original.Sources), len(unmarshaled.Sources))
assert.Equal(t, len(original.Destinations), len(unmarshaled.Destinations))
assert.Len(t, unmarshaled.Sources, len(original.Sources))
assert.Len(t, unmarshaled.Destinations, len(original.Destinations))
}
func TestACL_UnmarshalJSON_PolicyIntegration(t *testing.T) {
@ -3484,15 +3494,16 @@ func TestACL_UnmarshalJSON_InvalidAction(t *testing.T) {
_, err := unmarshalPolicy([]byte(policyJSON))
require.Error(t, err)
assert.Contains(t, err.Error(), `invalid action "deny"`)
assert.Contains(t, err.Error(), `invalid ACL action: "deny"`)
}
// Helper function to parse aliases for testing
// Helper function to parse aliases for testing.
func mustParseAlias(s string) Alias {
alias, err := parseAlias(s)
if err != nil {
panic(err)
}
return alias
}

View file

@ -9,6 +9,18 @@ import (
"tailscale.com/tailcfg"
)
// Port parsing errors.
var (
ErrInputMissingColon = errors.New("input must contain a colon character separating destination and port")
ErrInputStartsWithColon = errors.New("input cannot start with a colon character")
ErrInputEndsWithColon = errors.New("input cannot end with a colon character")
ErrInvalidPortRangeFormat = errors.New("invalid port range format")
ErrPortRangeInverted = errors.New("invalid port range: first port is greater than last port")
ErrPortMustBePositive = errors.New("first port must be >0, or use '*' for wildcard")
ErrInvalidPortNumber = errors.New("invalid port number")
ErrPortNumberOutOfRange = errors.New("port number out of range")
)
// splitDestinationAndPort takes an input string and returns the destination and port as a tuple, or an error if the input is invalid.
func splitDestinationAndPort(input string) (string, string, error) {
// Find the last occurrence of the colon character
@ -16,13 +28,15 @@ func splitDestinationAndPort(input string) (string, string, error) {
// Check if the colon character is present and not at the beginning or end of the string
if lastColonIndex == -1 {
return "", "", errors.New("input must contain a colon character separating destination and port")
return "", "", ErrInputMissingColon
}
if lastColonIndex == 0 {
return "", "", errors.New("input cannot start with a colon character")
return "", "", ErrInputStartsWithColon
}
if lastColonIndex == len(input)-1 {
return "", "", errors.New("input cannot end with a colon character")
return "", "", ErrInputEndsWithColon
}
// Split the string into destination and port based on the last colon
@ -45,11 +59,12 @@ func parsePortRange(portDef string) ([]tailcfg.PortRange, error) {
for part := range parts {
if strings.Contains(part, "-") {
rangeParts := strings.Split(part, "-")
rangeParts = slices.DeleteFunc(rangeParts, func(e string) bool {
return e == ""
})
if len(rangeParts) != 2 {
return nil, errors.New("invalid port range format")
return nil, ErrInvalidPortRangeFormat
}
first, err := parsePort(rangeParts[0])
@ -63,7 +78,7 @@ func parsePortRange(portDef string) ([]tailcfg.PortRange, error) {
}
if first > last {
return nil, errors.New("invalid port range: first port is greater than last port")
return nil, ErrPortRangeInverted
}
portRanges = append(portRanges, tailcfg.PortRange{First: first, Last: last})
@ -74,7 +89,7 @@ func parsePortRange(portDef string) ([]tailcfg.PortRange, error) {
}
if port < 1 {
return nil, errors.New("first port must be >0, or use '*' for wildcard")
return nil, ErrPortMustBePositive
}
portRanges = append(portRanges, tailcfg.PortRange{First: port, Last: port})
@ -88,11 +103,11 @@ func parsePortRange(portDef string) ([]tailcfg.PortRange, error) {
func parsePort(portStr string) (uint16, error) {
port, err := strconv.Atoi(portStr)
if err != nil {
return 0, errors.New("invalid port number")
return 0, ErrInvalidPortNumber
}
if port < 0 || port > 65535 {
return 0, errors.New("port number out of range")
return 0, ErrPortNumberOutOfRange
}
return uint16(port), nil

View file

@ -1,7 +1,6 @@
package v2
import (
"errors"
"testing"
"github.com/google/go-cmp/cmp"
@ -24,9 +23,9 @@ func TestParseDestinationAndPort(t *testing.T) {
{"tag:api-server:443", "tag:api-server", "443", nil},
{"example-host-1:*", "example-host-1", "*", nil},
{"hostname:80-90", "hostname", "80-90", nil},
{"invalidinput", "", "", errors.New("input must contain a colon character separating destination and port")},
{":invalid", "", "", errors.New("input cannot start with a colon character")},
{"invalid:", "", "", errors.New("input cannot end with a colon character")},
{"invalidinput", "", "", ErrInputMissingColon},
{":invalid", "", "", ErrInputStartsWithColon},
{"invalid:", "", "", ErrInputEndsWithColon},
}
for _, testCase := range testCases {
@ -58,9 +57,11 @@ func TestParsePort(t *testing.T) {
if err != nil && err.Error() != test.err {
t.Errorf("parsePort(%q) error = %v, expected error = %v", test.input, err, test.err)
}
if err == nil && test.err != "" {
t.Errorf("parsePort(%q) expected error = %v, got nil", test.input, test.err)
}
if result != test.expected {
t.Errorf("parsePort(%q) = %v, expected %v", test.input, result, test.expected)
}
@ -92,9 +93,11 @@ func TestParsePortRange(t *testing.T) {
if err != nil && err.Error() != test.err {
t.Errorf("parsePortRange(%q) error = %v, expected error = %v", test.input, err, test.err)
}
if err == nil && test.err != "" {
t.Errorf("parsePortRange(%q) expected error = %v, got nil", test.input, test.err)
}
if diff := cmp.Diff(result, test.expected); diff != "" {
t.Errorf("parsePortRange(%q) mismatch (-want +got):\n%s", test.input, diff)
}