all: fix golangci-lint issues (#3064)
This commit is contained in:
parent
bfb6fd80df
commit
ce580f8245
131 changed files with 3131 additions and 1560 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue