mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-20 15:51:40 +02:00
policy/v2: add unit tests for grant filter compilation helpers
Test companionCapGrantRules, sourcesHaveWildcard, sourcesHaveDangerAll, srcIPsWithRoutes, the FilterAllowAll fix for grant-only policies, compileViaGrant, compileGrantWithAutogroupSelf grant paths, and destinationsToNetPortRange autogroup:internet skipping. 51 subtests across 8 test functions covering all grant-specific code paths in filter.go that previously had no test coverage. Updates #2180
This commit is contained in:
@@ -3172,3 +3172,950 @@ func TestGroupSourcesByUser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompanionCapGrantRules(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dstIPStrings []string
|
||||||
|
srcPrefixes []netip.Prefix
|
||||||
|
capMap tailcfg.PeerCapMap
|
||||||
|
want []tailcfg.FilterRule
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "drive produces drive-sharer companion with reversed IPs",
|
||||||
|
dstIPStrings: []string{"100.64.0.1"},
|
||||||
|
srcPrefixes: []netip.Prefix{mp("100.64.0.2/32")},
|
||||||
|
capMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityTaildrive: {tailcfg.RawMessage(`{}`)},
|
||||||
|
},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.1"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{
|
||||||
|
{
|
||||||
|
Dsts: []netip.Prefix{mp("100.64.0.2/32")},
|
||||||
|
CapMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityTaildriveSharer: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relay produces relay-target companion with reversed IPs",
|
||||||
|
dstIPStrings: []string{"100.64.0.10"},
|
||||||
|
srcPrefixes: []netip.Prefix{mp("100.64.0.20/32")},
|
||||||
|
capMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityRelay: {tailcfg.RawMessage(`{}`)},
|
||||||
|
},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.10"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{
|
||||||
|
{
|
||||||
|
Dsts: []netip.Prefix{mp("100.64.0.20/32")},
|
||||||
|
CapMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityRelayTarget: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both drive and relay sorted by original cap name",
|
||||||
|
dstIPStrings: []string{"100.64.0.1"},
|
||||||
|
srcPrefixes: []netip.Prefix{mp("100.64.0.2/32")},
|
||||||
|
capMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityRelay: {tailcfg.RawMessage(`{}`)},
|
||||||
|
tailcfg.PeerCapabilityTaildrive: {tailcfg.RawMessage(`{}`)},
|
||||||
|
},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
// drive < relay alphabetically
|
||||||
|
SrcIPs: []string{"100.64.0.1"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{
|
||||||
|
{
|
||||||
|
Dsts: []netip.Prefix{mp("100.64.0.2/32")},
|
||||||
|
CapMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityTaildriveSharer: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.1"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{
|
||||||
|
{
|
||||||
|
Dsts: []netip.Prefix{mp("100.64.0.2/32")},
|
||||||
|
CapMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityRelayTarget: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown capability produces no companion",
|
||||||
|
dstIPStrings: []string{"100.64.0.1"},
|
||||||
|
srcPrefixes: []netip.Prefix{mp("100.64.0.2/32")},
|
||||||
|
capMap: tailcfg.PeerCapMap{
|
||||||
|
"example.com/cap/custom": {tailcfg.RawMessage(`{}`)},
|
||||||
|
},
|
||||||
|
want: []tailcfg.FilterRule{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "companion has nil CapMap value not original",
|
||||||
|
dstIPStrings: []string{"100.64.0.5"},
|
||||||
|
srcPrefixes: []netip.Prefix{mp("100.64.0.6/32")},
|
||||||
|
capMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityTaildrive: {
|
||||||
|
tailcfg.RawMessage(`{"access":"rw"}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.5"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{
|
||||||
|
{
|
||||||
|
Dsts: []netip.Prefix{mp("100.64.0.6/32")},
|
||||||
|
CapMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityTaildriveSharer: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple IP ranges reversed correctly",
|
||||||
|
dstIPStrings: []string{
|
||||||
|
"100.64.0.10",
|
||||||
|
"100.64.0.11",
|
||||||
|
},
|
||||||
|
srcPrefixes: []netip.Prefix{
|
||||||
|
mp("100.64.0.20/32"),
|
||||||
|
mp("100.64.0.21/32"),
|
||||||
|
},
|
||||||
|
capMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityRelay: {tailcfg.RawMessage(`{}`)},
|
||||||
|
},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.10", "100.64.0.11"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{
|
||||||
|
{
|
||||||
|
Dsts: []netip.Prefix{
|
||||||
|
mp("100.64.0.20/32"),
|
||||||
|
mp("100.64.0.21/32"),
|
||||||
|
},
|
||||||
|
CapMap: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityRelayTarget: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := companionCapGrantRules(tt.dstIPStrings, tt.srcPrefixes, tt.capMap)
|
||||||
|
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
|
||||||
|
t.Errorf("companionCapGrantRules() mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourcesHaveWildcard(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
srcs Aliases
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "wildcard only",
|
||||||
|
srcs: Aliases{Wildcard},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard mixed with specific",
|
||||||
|
srcs: Aliases{up("user@"), Wildcard, tp("tag:server")},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no wildcard",
|
||||||
|
srcs: Aliases{up("user@"), tp("tag:server")},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
srcs: Aliases{},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assert.Equal(t, tt.want, sourcesHaveWildcard(tt.srcs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourcesHaveDangerAll(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
srcs Aliases
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "danger-all only",
|
||||||
|
srcs: Aliases{agp(string(AutoGroupDangerAll))},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "danger-all mixed with others",
|
||||||
|
srcs: Aliases{up("user@"), agp(string(AutoGroupDangerAll))},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no danger-all",
|
||||||
|
srcs: Aliases{up("user@"), agp(string(AutoGroupMember))},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
srcs: Aliases{},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assert.Equal(t, tt.want, sourcesHaveDangerAll(tt.srcs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSrcIPsWithRoutes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Build a resolved address set for a single IP.
|
||||||
|
var b netipx.IPSetBuilder
|
||||||
|
b.AddPrefix(netip.MustParsePrefix("100.64.0.1/32"))
|
||||||
|
|
||||||
|
resolved, err := newResolved(&b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Node with approved subnet route.
|
||||||
|
nodeWithRoutes := types.Nodes{
|
||||||
|
&types.Node{
|
||||||
|
IPv4: ap("100.64.0.5"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{
|
||||||
|
mp("10.0.0.0/24"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{
|
||||||
|
mp("10.0.0.0/24"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}.ViewSlice()
|
||||||
|
|
||||||
|
emptyNodes := types.Nodes{}.ViewSlice()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
resolved ResolvedAddresses
|
||||||
|
hasWildcard bool
|
||||||
|
hasDangerAll bool
|
||||||
|
nodes func() []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "danger-all returns star regardless",
|
||||||
|
resolved: resolved,
|
||||||
|
hasWildcard: false,
|
||||||
|
hasDangerAll: true,
|
||||||
|
want: []string{"*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "danger-all takes precedence over wildcard",
|
||||||
|
resolved: resolved,
|
||||||
|
hasWildcard: true,
|
||||||
|
hasDangerAll: true,
|
||||||
|
want: []string{"*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard appends approved subnet routes",
|
||||||
|
resolved: resolved,
|
||||||
|
hasWildcard: true,
|
||||||
|
hasDangerAll: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "neither returns resolved addrs only",
|
||||||
|
resolved: resolved,
|
||||||
|
hasWildcard: false,
|
||||||
|
hasDangerAll: false,
|
||||||
|
want: []string{"100.64.0.1"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
nodes := emptyNodes
|
||||||
|
if tt.hasWildcard && !tt.hasDangerAll {
|
||||||
|
nodes = nodeWithRoutes
|
||||||
|
}
|
||||||
|
|
||||||
|
got := srcIPsWithRoutes(tt.resolved, tt.hasWildcard, tt.hasDangerAll, nodes)
|
||||||
|
|
||||||
|
if tt.hasDangerAll {
|
||||||
|
assert.Equal(t, []string{"*"}, got)
|
||||||
|
} else if tt.hasWildcard {
|
||||||
|
assert.Contains(t, got, "100.64.0.1", "should contain the resolved IP")
|
||||||
|
assert.Contains(t, got, "10.0.0.0/24", "should contain approved subnet route")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterAllowAllFix(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
users := types.Users{
|
||||||
|
{Model: gorm.Model{ID: 1}, Name: "testuser"},
|
||||||
|
}
|
||||||
|
nodes := types.Nodes{
|
||||||
|
&types.Node{
|
||||||
|
IPv4: ap("100.64.0.1"),
|
||||||
|
User: &users[0],
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
},
|
||||||
|
}.ViewSlice()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pol *Policy
|
||||||
|
wantFilterAllow bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "grants only should not return FilterAllowAll",
|
||||||
|
pol: &Policy{
|
||||||
|
Grants: []Grant{
|
||||||
|
{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{pp("100.64.0.1/32")},
|
||||||
|
InternetProtocols: []ProtocolPort{
|
||||||
|
{Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantFilterAllow: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil ACLs and nil grants returns FilterAllowAll",
|
||||||
|
pol: &Policy{},
|
||||||
|
wantFilterAllow: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil ACLs and empty grants returns FilterAllowAll",
|
||||||
|
pol: &Policy{
|
||||||
|
Grants: []Grant{},
|
||||||
|
},
|
||||||
|
wantFilterAllow: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "both ACLs and grants should not return FilterAllowAll",
|
||||||
|
pol: &Policy{
|
||||||
|
ACLs: []ACL{
|
||||||
|
{
|
||||||
|
Action: "accept",
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: []AliasWithPorts{
|
||||||
|
aliasWithPorts(pp("100.64.0.1/32"), tailcfg.PortRangeAny),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Grants: []Grant{
|
||||||
|
{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{pp("100.64.0.1/32")},
|
||||||
|
InternetProtocols: []ProtocolPort{
|
||||||
|
{Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantFilterAllow: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil policy returns FilterAllowAll",
|
||||||
|
pol: nil,
|
||||||
|
wantFilterAllow: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
rules, err := tt.pol.compileFilterRules(users, nodes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
isFilterAllowAll := cmp.Diff(tailcfg.FilterAllowAll, rules) == ""
|
||||||
|
assert.Equal(t, tt.wantFilterAllow, isFilterAllowAll,
|
||||||
|
"FilterAllowAll mismatch: got rules=%v", rules)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileViaGrant(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
users := types.Users{
|
||||||
|
{Model: gorm.Model{ID: 1}, Name: "testuser"},
|
||||||
|
}
|
||||||
|
|
||||||
|
allPorts := []ProtocolPort{
|
||||||
|
{Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node matching via tag with approved subnet routes.
|
||||||
|
viaNode := &types.Node{
|
||||||
|
IPv4: ap("100.64.0.1"),
|
||||||
|
User: &users[0],
|
||||||
|
Tags: []string{"tag:relay"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{
|
||||||
|
mp("10.0.0.0/24"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{
|
||||||
|
mp("10.0.0.0/24"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node matching via tag with exit routes (0.0.0.0/0, ::/0).
|
||||||
|
exitNode := &types.Node{
|
||||||
|
IPv4: ap("100.64.0.2"),
|
||||||
|
User: &users[0],
|
||||||
|
Tags: []string{"tag:exit"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{
|
||||||
|
RoutableIPs: []netip.Prefix{
|
||||||
|
mp("0.0.0.0/0"),
|
||||||
|
mp("::/0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ApprovedRoutes: []netip.Prefix{
|
||||||
|
mp("0.0.0.0/0"),
|
||||||
|
mp("::/0"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node matching via tag but no advertised routes.
|
||||||
|
taggedNoRoutes := &types.Node{
|
||||||
|
IPv4: ap("100.64.0.3"),
|
||||||
|
User: &users[0],
|
||||||
|
Tags: []string{"tag:relay"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node not matching any via tag.
|
||||||
|
nonViaNode := &types.Node{
|
||||||
|
IPv4: ap("100.64.0.4"),
|
||||||
|
User: &users[0],
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source node with IP.
|
||||||
|
srcNode := &types.Node{
|
||||||
|
IPv4: ap("100.64.0.10"),
|
||||||
|
User: &users[0],
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
grant Grant
|
||||||
|
node *types.Node
|
||||||
|
nodes types.Nodes
|
||||||
|
pol *Policy
|
||||||
|
want []tailcfg.FilterRule
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "node not matching via tag returns nil",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: nonViaNode,
|
||||||
|
nodes: types.Nodes{nonViaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node matching via tag no advertised routes returns nil",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: taggedNoRoutes,
|
||||||
|
nodes: types.Nodes{taggedNoRoutes, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node matching via tag with matching subnet routes returns rules",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: viaNode,
|
||||||
|
nodes: types.Nodes{viaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.10"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{IP: "10.0.0.0/24", Ports: tailcfg.PortRangeAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autogroup:internet with exit routes produces rules",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{agp(string(AutoGroupInternet))},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:exit"},
|
||||||
|
},
|
||||||
|
node: exitNode,
|
||||||
|
nodes: types.Nodes{exitNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.10"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{IP: "0.0.0.0/0", Ports: tailcfg.PortRangeAny},
|
||||||
|
{IP: "::/0", Ports: tailcfg.PortRangeAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autogroup:internet without exit routes returns nil",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{agp(string(AutoGroupInternet))},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: viaNode,
|
||||||
|
nodes: types.Nodes{viaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autogroup:self in sources returns errSelfInSources",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{agp(string(AutoGroupSelf))},
|
||||||
|
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: viaNode,
|
||||||
|
nodes: types.Nodes{viaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
wantErr: errSelfInSources,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard sources include subnet routes in SrcIPs",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{Wildcard},
|
||||||
|
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: viaNode,
|
||||||
|
nodes: types.Nodes{viaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "danger-all sources produce SrcIPs star",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{agp(string(AutoGroupDangerAll))},
|
||||||
|
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: viaNode,
|
||||||
|
nodes: types.Nodes{viaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"*"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{IP: "10.0.0.0/24", Ports: tailcfg.PortRangeAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "app-only via grant with no ip field returns nil",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||||
|
App: tailcfg.PeerCapMap{
|
||||||
|
tailcfg.PeerCapabilityRelay: {tailcfg.RawMessage(`{}`)},
|
||||||
|
},
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: viaNode,
|
||||||
|
nodes: types.Nodes{viaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple destinations some matching some not",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("testuser@")},
|
||||||
|
Destinations: Aliases{
|
||||||
|
pp("10.0.0.0/24"), // matches viaNode route
|
||||||
|
pp("192.168.0.0/16"), // does not match viaNode route
|
||||||
|
},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
Via: []Tag{"tag:relay"},
|
||||||
|
},
|
||||||
|
node: viaNode,
|
||||||
|
nodes: types.Nodes{viaNode, srcNode},
|
||||||
|
pol: &Policy{},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.0.10"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{IP: "10.0.0.0/24", Ports: tailcfg.PortRangeAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
nodeView := tt.node.View()
|
||||||
|
nodesSlice := tt.nodes.ViewSlice()
|
||||||
|
|
||||||
|
got, err := tt.pol.compileViaGrant(tt.grant, users, nodeView, nodesSlice)
|
||||||
|
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if tt.name == "wildcard sources include subnet routes in SrcIPs" {
|
||||||
|
// Wildcard resolves to CGNAT ranges; just check the route is appended.
|
||||||
|
require.Len(t, got, 1)
|
||||||
|
assert.Contains(t, got[0].SrcIPs, "10.0.0.0/24",
|
||||||
|
"wildcard SrcIPs should include approved subnet route")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||||
|
t.Errorf("compileViaGrant() mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileGrantWithAutogroupSelf_GrantPaths(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
users := types.Users{
|
||||||
|
{Model: gorm.Model{ID: 1}, Name: "user1"},
|
||||||
|
{Model: gorm.Model{ID: 2}, Name: "user2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
node1 := &types.Node{
|
||||||
|
User: new(users[0]),
|
||||||
|
IPv4: ap("100.64.0.1"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
node2 := &types.Node{
|
||||||
|
User: new(users[0]),
|
||||||
|
IPv4: ap("100.64.0.2"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
node3 := &types.Node{
|
||||||
|
User: new(users[1]),
|
||||||
|
IPv4: ap("100.64.0.3"),
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
taggedNode := &types.Node{
|
||||||
|
User: &users[0],
|
||||||
|
IPv4: ap("100.64.0.10"),
|
||||||
|
Tags: []string{"tag:server"},
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
allNodes := types.Nodes{node1, node2, node3, taggedNode}
|
||||||
|
|
||||||
|
allPorts := []ProtocolPort{
|
||||||
|
{Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
grant Grant
|
||||||
|
node *types.Node
|
||||||
|
pol *Policy
|
||||||
|
want []tailcfg.FilterRule
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty sources produces no rules",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{},
|
||||||
|
Destinations: Aliases{pp("100.64.0.3/32")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
},
|
||||||
|
node: node1,
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty destinations produces no rules",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("user1@")},
|
||||||
|
Destinations: Aliases{},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
},
|
||||||
|
node: node1,
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autogroup:self in sources returns errSelfInSources",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{agp(string(AutoGroupSelf))},
|
||||||
|
Destinations: Aliases{pp("100.64.0.3/32")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
},
|
||||||
|
node: node1,
|
||||||
|
pol: &Policy{},
|
||||||
|
wantErr: errSelfInSources,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autogroup:self destination for tagged node is skipped",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("user1@")},
|
||||||
|
Destinations: Aliases{agp(string(AutoGroupSelf))},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
},
|
||||||
|
node: taggedNode,
|
||||||
|
pol: &Policy{},
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autogroup:self destination for untagged node produces same-user devices",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("user1@")},
|
||||||
|
Destinations: Aliases{agp(string(AutoGroupSelf))},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
},
|
||||||
|
node: node1,
|
||||||
|
pol: &Policy{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "combined IP and App grant produces both DstPorts and CapGrant rules",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{up("user1@")},
|
||||||
|
Destinations: Aliases{pp("100.64.0.3/32")},
|
||||||
|
InternetProtocols: []ProtocolPort{
|
||||||
|
{Ports: []tailcfg.PortRange{tailcfg.PortRangeAny}},
|
||||||
|
},
|
||||||
|
App: tailcfg.PeerCapMap{
|
||||||
|
"example.com/cap/custom": {tailcfg.RawMessage(`{}`)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
node: node1,
|
||||||
|
pol: &Policy{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "danger-all in sources produces SrcIPs star",
|
||||||
|
grant: Grant{
|
||||||
|
Sources: Aliases{agp(string(AutoGroupDangerAll))},
|
||||||
|
Destinations: Aliases{pp("100.64.0.3/32")},
|
||||||
|
InternetProtocols: allPorts,
|
||||||
|
},
|
||||||
|
node: node1,
|
||||||
|
pol: &Policy{},
|
||||||
|
want: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"*"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{
|
||||||
|
{IP: "100.64.0.3", Ports: tailcfg.PortRangeAny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
nodeView := tt.node.View()
|
||||||
|
nodesSlice := allNodes.ViewSlice()
|
||||||
|
|
||||||
|
got, err := tt.pol.compileGrantWithAutogroupSelf(
|
||||||
|
tt.grant, users, nodeView, nodesSlice,
|
||||||
|
)
|
||||||
|
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
switch tt.name {
|
||||||
|
case "autogroup:self destination for untagged node produces same-user devices":
|
||||||
|
// Should produce rules; sources and destinations should only
|
||||||
|
// include user1's untagged devices (node1 and node2).
|
||||||
|
// IPs are merged into ranges by IPSet (e.g. "100.64.0.1-100.64.0.2").
|
||||||
|
require.NotEmpty(t, got, "expected rules for autogroup:self")
|
||||||
|
rule := got[0]
|
||||||
|
// SrcIPs from IPSet may be a merged range.
|
||||||
|
require.Len(t, rule.SrcIPs, 1)
|
||||||
|
assert.Equal(t, "100.64.0.1-100.64.0.2", rule.SrcIPs[0],
|
||||||
|
"SrcIPs should contain merged range for user1 untagged devices")
|
||||||
|
|
||||||
|
var destIPs []string
|
||||||
|
for _, dp := range rule.DstPorts {
|
||||||
|
destIPs = append(destIPs, dp.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DstPorts use individual IPs (not IPSet ranges).
|
||||||
|
assert.ElementsMatch(t, []string{"100.64.0.1", "100.64.0.2"}, destIPs,
|
||||||
|
"DstPorts should be user1 untagged devices only")
|
||||||
|
|
||||||
|
case "combined IP and App grant produces both DstPorts and CapGrant rules":
|
||||||
|
hasDstPorts := false
|
||||||
|
hasCapGrant := false
|
||||||
|
|
||||||
|
for _, rule := range got {
|
||||||
|
if len(rule.DstPorts) > 0 {
|
||||||
|
hasDstPorts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rule.CapGrant) > 0 {
|
||||||
|
hasCapGrant = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, hasDstPorts, "should have rules with DstPorts")
|
||||||
|
assert.True(t, hasCapGrant, "should have rules with CapGrant")
|
||||||
|
|
||||||
|
default:
|
||||||
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
||||||
|
t.Errorf("compileGrantWithAutogroupSelf() mismatch (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDestinationsToNetPortRange_AutogroupInternet(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
users := types.Users{
|
||||||
|
{Model: gorm.Model{ID: 1}, Name: "testuser"},
|
||||||
|
}
|
||||||
|
nodes := types.Nodes{
|
||||||
|
&types.Node{
|
||||||
|
IPv4: ap("100.64.0.1"),
|
||||||
|
User: &users[0],
|
||||||
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
|
},
|
||||||
|
}.ViewSlice()
|
||||||
|
|
||||||
|
pol := &Policy{}
|
||||||
|
ports := []tailcfg.PortRange{tailcfg.PortRangeAny}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dests Aliases
|
||||||
|
wantLen int
|
||||||
|
wantStar bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "autogroup:internet produces no DstPorts",
|
||||||
|
dests: Aliases{agp(string(AutoGroupInternet))},
|
||||||
|
wantLen: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wildcard produces DstPorts with star",
|
||||||
|
dests: Aliases{Wildcard},
|
||||||
|
wantLen: 1,
|
||||||
|
wantStar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit prefix produces DstPorts",
|
||||||
|
dests: Aliases{pp("100.64.0.1/32")},
|
||||||
|
wantLen: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
got := pol.destinationsToNetPortRange(users, nodes, tt.dests, ports)
|
||||||
|
assert.Len(t, got, tt.wantLen)
|
||||||
|
|
||||||
|
if tt.wantStar && len(got) > 0 {
|
||||||
|
assert.Equal(t, "*", got[0].IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tt.wantStar && tt.wantLen > 0 && len(got) > 0 {
|
||||||
|
assert.NotEqual(t, "*", got[0].IP)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user