mirror of
https://github.com/juanfont/headscale.git
synced 2024-12-18 14:07:34 +00:00
4055 lines
90 KiB
Go
4055 lines
90 KiB
Go
package policy
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"math/rand/v2"
|
|
"net/netip"
|
|
"slices"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/juanfont/headscale/hscontrol/util"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/spf13/viper"
|
|
"github.com/stretchr/testify/require"
|
|
"go4.org/netipx"
|
|
"gopkg.in/check.v1"
|
|
"gorm.io/gorm"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/tailcfg"
|
|
)
|
|
|
|
var iap = func(ipStr string) *netip.Addr {
|
|
ip := netip.MustParseAddr(ipStr)
|
|
return &ip
|
|
}
|
|
|
|
func Test(t *testing.T) {
|
|
check.TestingT(t)
|
|
}
|
|
|
|
var _ = check.Suite(&Suite{})
|
|
|
|
type Suite struct{}
|
|
|
|
func (s *Suite) TestWrongPath(c *check.C) {
|
|
_, err := LoadACLPolicyFromPath("asdfg")
|
|
c.Assert(err, check.NotNil)
|
|
}
|
|
|
|
func TestParsing(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
format string
|
|
acl string
|
|
want []tailcfg.FilterRule
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "invalid-hujson",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
`,
|
|
want: []tailcfg.FilterRule{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "valid-hujson-invalid-content",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"valid_json": true,
|
|
"but_a_policy_though": false
|
|
}
|
|
`,
|
|
want: []tailcfg.FilterRule{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid-cidr",
|
|
format: "hujson",
|
|
acl: `
|
|
{"example-host-1": "100.100.100.100/42"}
|
|
`,
|
|
want: []tailcfg.FilterRule{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "basic-rule",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"hosts": {
|
|
"host-1": "100.100.100.100",
|
|
"subnet-1": "100.100.101.100/24",
|
|
},
|
|
|
|
"acls": [
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"subnet-1",
|
|
"192.168.1.0/24"
|
|
],
|
|
"dst": [
|
|
"*:22,3389",
|
|
"host-1:*",
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`,
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"100.100.101.0/24", "192.168.1.0/24"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
|
|
{IP: "::/0", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
|
{IP: "::/0", Ports: tailcfg.PortRange{First: 3389, Last: 3389}},
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "parse-protocol",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"hosts": {
|
|
"host-1": "100.100.100.100",
|
|
"subnet-1": "100.100.101.100/24",
|
|
},
|
|
|
|
"acls": [
|
|
{
|
|
"Action": "accept",
|
|
"src": [
|
|
"*",
|
|
],
|
|
"proto": "tcp",
|
|
"dst": [
|
|
"host-1:*",
|
|
],
|
|
},
|
|
{
|
|
"Action": "accept",
|
|
"src": [
|
|
"*",
|
|
],
|
|
"proto": "udp",
|
|
"dst": [
|
|
"host-1:53",
|
|
],
|
|
},
|
|
{
|
|
"Action": "accept",
|
|
"src": [
|
|
"*",
|
|
],
|
|
"proto": "icmp",
|
|
"dst": [
|
|
"host-1:*",
|
|
],
|
|
},
|
|
],
|
|
}`,
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
IPProto: []int{protocolTCP},
|
|
},
|
|
{
|
|
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRange{First: 53, Last: 53}},
|
|
},
|
|
IPProto: []int{protocolUDP},
|
|
},
|
|
{
|
|
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
IPProto: []int{protocolICMP, protocolIPv6ICMP},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "port-wildcard",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"hosts": {
|
|
"host-1": "100.100.100.100",
|
|
"subnet-1": "100.100.101.100/24",
|
|
},
|
|
|
|
"acls": [
|
|
{
|
|
"Action": "accept",
|
|
"src": [
|
|
"*",
|
|
],
|
|
"dst": [
|
|
"host-1:*",
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`,
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "port-range",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"hosts": {
|
|
"host-1": "100.100.100.100",
|
|
"subnet-1": "100.100.101.100/24",
|
|
},
|
|
|
|
"acls": [
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"subnet-1",
|
|
],
|
|
"dst": [
|
|
"host-1:5400-5500",
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`,
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"100.100.101.0/24"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.100.100.100/32",
|
|
Ports: tailcfg.PortRange{First: 5400, Last: 5500},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "port-group",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"groups": {
|
|
"group:example": [
|
|
"testuser",
|
|
],
|
|
},
|
|
|
|
"hosts": {
|
|
"host-1": "100.100.100.100",
|
|
"subnet-1": "100.100.101.100/24",
|
|
},
|
|
|
|
"acls": [
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"group:example",
|
|
],
|
|
"dst": [
|
|
"host-1:*",
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`,
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"200.200.200.200/32"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "port-user",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"hosts": {
|
|
"host-1": "100.100.100.100",
|
|
"subnet-1": "100.100.101.100/24",
|
|
},
|
|
|
|
"acls": [
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"testuser",
|
|
],
|
|
"dst": [
|
|
"host-1:*",
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`,
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"200.200.200.200/32"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ipv6",
|
|
format: "hujson",
|
|
acl: `
|
|
{
|
|
"hosts": {
|
|
"host-1": "100.100.100.100/32",
|
|
"subnet-1": "100.100.101.100/24",
|
|
},
|
|
|
|
"acls": [
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"*",
|
|
],
|
|
"dst": [
|
|
"host-1:*",
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`,
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.100.100.100/32", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pol, err := LoadACLPolicyFromBytes([]byte(tt.acl))
|
|
|
|
if tt.wantErr && err == nil {
|
|
t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
return
|
|
} else if !tt.wantErr && err != nil {
|
|
t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
user := types.User{
|
|
Model: gorm.Model{ID: 1},
|
|
Name: "testuser",
|
|
}
|
|
rules, err := pol.CompileFilterRules(
|
|
[]types.User{
|
|
user,
|
|
},
|
|
types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.100.100.100"),
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("200.200.200.200"),
|
|
User: user,
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
})
|
|
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("parsing() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
return
|
|
}
|
|
|
|
if diff := cmp.Diff(tt.want, rules); diff != "" {
|
|
t.Errorf("parsing() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *Suite) TestRuleInvalidGeneration(c *check.C) {
|
|
acl := []byte(`
|
|
{
|
|
// Declare static groups of users beyond those in the identity service.
|
|
"groups": {
|
|
"group:example": [
|
|
"user1@example.com",
|
|
"user2@example.com",
|
|
],
|
|
},
|
|
// Declare hostname aliases to use in place of IP addresses or subnets.
|
|
"hosts": {
|
|
"example-host-1": "100.100.100.100",
|
|
"example-host-2": "100.100.101.100/24",
|
|
},
|
|
// Define who is allowed to use which tags.
|
|
"tagOwners": {
|
|
// Everyone in the montreal-admins or global-admins group are
|
|
// allowed to tag servers as montreal-webserver.
|
|
"tag:montreal-webserver": [
|
|
"group:montreal-admins",
|
|
"group:global-admins",
|
|
],
|
|
// Only a few admins are allowed to create API servers.
|
|
"tag:api-server": [
|
|
"group:global-admins",
|
|
"example-host-1",
|
|
],
|
|
},
|
|
// Access control lists.
|
|
"acls": [
|
|
// Engineering users, plus the president, can access port 22 (ssh)
|
|
// and port 3389 (remote desktop protocol) on all servers, and all
|
|
// ports on git-server or ci-server.
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"group:engineering",
|
|
"president@example.com"
|
|
],
|
|
"dst": [
|
|
"*:22,3389",
|
|
"git-server:*",
|
|
"ci-server:*"
|
|
],
|
|
},
|
|
// Allow engineer users to access any port on a device tagged with
|
|
// tag:production.
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"group:engineers"
|
|
],
|
|
"dst": [
|
|
"tag:production:*"
|
|
],
|
|
},
|
|
// Allow servers in the my-subnet host and 192.168.1.0/24 to access hosts
|
|
// on both networks.
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"my-subnet",
|
|
"192.168.1.0/24"
|
|
],
|
|
"dst": [
|
|
"my-subnet:*",
|
|
"192.168.1.0/24:*"
|
|
],
|
|
},
|
|
// Allow every user of your network to access anything on the network.
|
|
// Comment out this section if you want to define specific ACL
|
|
// restrictions above.
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"*"
|
|
],
|
|
"dst": [
|
|
"*:*"
|
|
],
|
|
},
|
|
// All users in Montreal are allowed to access the Montreal web
|
|
// servers.
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"group:montreal-users"
|
|
],
|
|
"dst": [
|
|
"tag:montreal-webserver:80,443"
|
|
],
|
|
},
|
|
// Montreal web servers are allowed to make outgoing connections to
|
|
// the API servers, but only on https port 443.
|
|
// In contrast, this doesn't grant API servers the right to initiate
|
|
// any connections.
|
|
{
|
|
"action": "accept",
|
|
"src": [
|
|
"tag:montreal-webserver"
|
|
],
|
|
"dst": [
|
|
"tag:api-server:443"
|
|
],
|
|
},
|
|
],
|
|
// Declare tests to check functionality of ACL rules
|
|
"tests": [
|
|
{
|
|
"src": "user1@example.com",
|
|
"accept": [
|
|
"example-host-1:22",
|
|
"example-host-2:80"
|
|
],
|
|
"deny": [
|
|
"example-host-2:100"
|
|
],
|
|
},
|
|
{
|
|
"src": "user2@example.com",
|
|
"accept": [
|
|
"100.60.3.4:22"
|
|
],
|
|
},
|
|
],
|
|
}
|
|
`)
|
|
pol, err := LoadACLPolicyFromBytes(acl)
|
|
c.Assert(pol.ACLs, check.HasLen, 6)
|
|
c.Assert(err, check.IsNil)
|
|
|
|
rules, err := pol.CompileFilterRules([]types.User{}, types.Nodes{})
|
|
c.Assert(err, check.NotNil)
|
|
c.Assert(rules, check.IsNil)
|
|
}
|
|
|
|
// TODO(kradalby): Make tests values safe, independent and descriptive.
|
|
func (s *Suite) TestInvalidAction(c *check.C) {
|
|
pol := &ACLPolicy{
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "invalidAction",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
}
|
|
_, _, err := GenerateFilterAndSSHRulesForTests(
|
|
pol,
|
|
&types.Node{},
|
|
types.Nodes{},
|
|
[]types.User{},
|
|
)
|
|
c.Assert(errors.Is(err, ErrInvalidAction), check.Equals, true)
|
|
}
|
|
|
|
func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
|
|
// this ACL is wrong because the group in Sources sections doesn't exist
|
|
pol := &ACLPolicy{
|
|
Groups: Groups{
|
|
"group:test": []string{"foo"},
|
|
"group:error": []string{"foo", "group:test"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:error"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
}
|
|
_, _, err := GenerateFilterAndSSHRulesForTests(
|
|
pol,
|
|
&types.Node{},
|
|
types.Nodes{},
|
|
[]types.User{},
|
|
)
|
|
c.Assert(errors.Is(err, ErrInvalidGroup), check.Equals, true)
|
|
}
|
|
|
|
func (s *Suite) TestInvalidTagOwners(c *check.C) {
|
|
// this ACL is wrong because no tagOwners own the requested tag for the server
|
|
pol := &ACLPolicy{
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"tag:foo"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
_, _, err := GenerateFilterAndSSHRulesForTests(
|
|
pol,
|
|
&types.Node{},
|
|
types.Nodes{},
|
|
[]types.User{},
|
|
)
|
|
c.Assert(errors.Is(err, ErrInvalidTag), check.Equals, true)
|
|
}
|
|
|
|
func Test_expandGroup(t *testing.T) {
|
|
type field struct {
|
|
pol ACLPolicy
|
|
}
|
|
type args struct {
|
|
group string
|
|
stripEmail bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
field field
|
|
args args
|
|
want []string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple test",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Groups: Groups{
|
|
"group:test": []string{"user1", "user2", "user3"},
|
|
"group:foo": []string{"user2", "user3"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
group: "group:test",
|
|
},
|
|
want: []string{"user1", "user2", "user3"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "InexistentGroup",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Groups: Groups{
|
|
"group:test": []string{"user1", "user2", "user3"},
|
|
"group:foo": []string{"user2", "user3"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
group: "group:undefined",
|
|
},
|
|
want: []string{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Expand emails in group",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Groups: Groups{
|
|
"group:admin": []string{
|
|
"joe.bar@gmail.com",
|
|
"john.doe@yahoo.fr",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
group: "group:admin",
|
|
},
|
|
want: []string{"joe.bar@gmail.com", "john.doe@yahoo.fr"},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
viper.Set("oidc.strip_email_domain", test.args.stripEmail)
|
|
|
|
got, err := test.field.pol.expandUsersFromGroup(
|
|
test.args.group,
|
|
)
|
|
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
|
|
|
|
return
|
|
}
|
|
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Errorf("expandGroup() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_expandTagOwners(t *testing.T) {
|
|
type args struct {
|
|
aclPolicy *ACLPolicy
|
|
tag string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want []string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple tag expansion",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
|
},
|
|
tag: "tag:test",
|
|
},
|
|
want: []string{"user1"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "expand with tag and group",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
Groups: Groups{"group:foo": []string{"user1", "user2"}},
|
|
TagOwners: TagOwners{"tag:test": []string{"group:foo"}},
|
|
},
|
|
tag: "tag:test",
|
|
},
|
|
want: []string{"user1", "user2"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "expand with user and group",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
Groups: Groups{"group:foo": []string{"user1", "user2"}},
|
|
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}},
|
|
},
|
|
tag: "tag:test",
|
|
},
|
|
want: []string{"user1", "user2", "user3"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid tag",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}},
|
|
},
|
|
tag: "tag:test",
|
|
},
|
|
want: []string{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid group",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
Groups: Groups{"group:bar": []string{"user1", "user2"}},
|
|
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}},
|
|
},
|
|
tag: "tag:test",
|
|
},
|
|
want: []string{},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := expandOwnersFromTag(
|
|
test.args.aclPolicy,
|
|
test.args.tag,
|
|
)
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
|
|
|
|
return
|
|
}
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Errorf("expandTagOwners() = (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_expandPorts(t *testing.T) {
|
|
type args struct {
|
|
portsStr string
|
|
needsWildcard bool
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want *[]tailcfg.PortRange
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "wildcard",
|
|
args: args{portsStr: "*", needsWildcard: true},
|
|
want: &[]tailcfg.PortRange{
|
|
{First: portRangeBegin, Last: portRangeEnd},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "needs wildcard but does not require it",
|
|
args: args{portsStr: "*", needsWildcard: false},
|
|
want: &[]tailcfg.PortRange{
|
|
{First: portRangeBegin, Last: portRangeEnd},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "needs wildcard but gets port",
|
|
args: args{portsStr: "80,443", needsWildcard: true},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "two Destinations",
|
|
args: args{portsStr: "80,443", needsWildcard: false},
|
|
want: &[]tailcfg.PortRange{
|
|
{First: 80, Last: 80},
|
|
{First: 443, Last: 443},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "a range and a port",
|
|
args: args{portsStr: "80-1024,443", needsWildcard: false},
|
|
want: &[]tailcfg.PortRange{
|
|
{First: 80, Last: 1024},
|
|
{First: 443, Last: 443},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "out of bounds",
|
|
args: args{portsStr: "854038", needsWildcard: false},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong port",
|
|
args: args{portsStr: "85a38", needsWildcard: false},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong port in first",
|
|
args: args{portsStr: "a-80", needsWildcard: false},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong port in last",
|
|
args: args{portsStr: "80-85a38", needsWildcard: false},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong port format",
|
|
args: args{portsStr: "80-85a38-3", needsWildcard: false},
|
|
want: nil,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := expandPorts(test.args.portsStr, test.args.needsWildcard)
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("expandPorts() error = %v, wantErr %v", err, test.wantErr)
|
|
|
|
return
|
|
}
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Errorf("expandPorts() = (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_filterNodesByUser(t *testing.T) {
|
|
users := []types.User{
|
|
{Model: gorm.Model{ID: 1}, Name: "marc"},
|
|
{Model: gorm.Model{ID: 2}, Name: "joe", Email: "joe@headscale.net"},
|
|
{
|
|
Model: gorm.Model{ID: 3},
|
|
Name: "mikael",
|
|
Email: "mikael@headscale.net",
|
|
ProviderIdentifier: sql.NullString{String: "http://oidc.org/1234", Valid: true},
|
|
},
|
|
{Model: gorm.Model{ID: 4}, Name: "mikael2", Email: "mikael@headscale.net"},
|
|
{Model: gorm.Model{ID: 5}, Name: "mikael", Email: "mikael2@headscale.net"},
|
|
{Model: gorm.Model{ID: 6}, Name: "http://oidc.org/1234", Email: "mikael@headscale.net"},
|
|
{Model: gorm.Model{ID: 7}, Name: "1"},
|
|
{Model: gorm.Model{ID: 8}, Name: "alex", Email: "alex@headscale.net"},
|
|
{Model: gorm.Model{ID: 9}, Name: "alex@headscale.net"},
|
|
{Model: gorm.Model{ID: 10}, Email: "http://oidc.org/1234"},
|
|
}
|
|
|
|
type args struct {
|
|
nodes types.Nodes
|
|
user string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want types.Nodes
|
|
}{
|
|
{
|
|
name: "1 node in user",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{User: users[1]},
|
|
},
|
|
user: "joe",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{User: users[1]},
|
|
},
|
|
},
|
|
{
|
|
name: "3 nodes, 2 in user",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[1]},
|
|
&types.Node{ID: 2, User: users[0]},
|
|
&types.Node{ID: 3, User: users[0]},
|
|
},
|
|
user: "marc",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{ID: 2, User: users[0]},
|
|
&types.Node{ID: 3, User: users[0]},
|
|
},
|
|
},
|
|
{
|
|
name: "5 nodes, 0 in user",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[1]},
|
|
&types.Node{ID: 2, User: users[0]},
|
|
&types.Node{ID: 3, User: users[0]},
|
|
&types.Node{ID: 4, User: users[0]},
|
|
&types.Node{ID: 5, User: users[0]},
|
|
},
|
|
user: "mickael",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "match-by-provider-ident",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[1]},
|
|
&types.Node{ID: 2, User: users[2]},
|
|
},
|
|
user: "http://oidc.org/1234",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{ID: 2, User: users[2]},
|
|
},
|
|
},
|
|
{
|
|
name: "match-by-email",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[1]},
|
|
&types.Node{ID: 2, User: users[2]},
|
|
&types.Node{ID: 8, User: users[7]},
|
|
},
|
|
user: "joe@headscale.net",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{ID: 1, User: users[1]},
|
|
},
|
|
},
|
|
{
|
|
name: "multi-match-is-zero",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[1]},
|
|
&types.Node{ID: 2, User: users[2]},
|
|
&types.Node{ID: 3, User: users[3]},
|
|
},
|
|
user: "mikael@headscale.net",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "multi-email-first-match-is-zero",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
// First match email, then provider id
|
|
&types.Node{ID: 3, User: users[3]},
|
|
&types.Node{ID: 2, User: users[2]},
|
|
},
|
|
user: "mikael@headscale.net",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "multi-username-first-match-is-zero",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
// First match username, then provider id
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 2, User: users[2]},
|
|
},
|
|
user: "mikael",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "all-users-duplicate-username-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
},
|
|
user: "mikael",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "all-users-unique-username-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
},
|
|
user: "marc",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
},
|
|
},
|
|
{
|
|
name: "all-users-no-username-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
},
|
|
user: "not-working",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "all-users-duplicate-email-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
},
|
|
user: "mikael@headscale.net",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "all-users-duplicate-email-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
&types.Node{ID: 8, User: users[7]},
|
|
},
|
|
user: "joe@headscale.net",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{ID: 2, User: users[1]},
|
|
},
|
|
},
|
|
{
|
|
name: "email-as-username-duplicate",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[7]},
|
|
&types.Node{ID: 2, User: users[8]},
|
|
},
|
|
user: "alex@headscale.net",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "all-users-no-email-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
},
|
|
user: "not-working@headscale.net",
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "all-users-provider-id-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
&types.Node{ID: 6, User: users[5]},
|
|
},
|
|
user: "http://oidc.org/1234",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{ID: 3, User: users[2]},
|
|
},
|
|
},
|
|
{
|
|
name: "all-users-no-provider-id-random-order",
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{ID: 1, User: users[0]},
|
|
&types.Node{ID: 2, User: users[1]},
|
|
&types.Node{ID: 3, User: users[2]},
|
|
&types.Node{ID: 4, User: users[3]},
|
|
&types.Node{ID: 5, User: users[4]},
|
|
&types.Node{ID: 6, User: users[5]},
|
|
},
|
|
user: "http://oidc.org/4321",
|
|
},
|
|
want: nil,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
for range 1000 {
|
|
ns := test.args.nodes
|
|
rand.Shuffle(len(ns), func(i, j int) {
|
|
ns[i], ns[j] = ns[j], ns[i]
|
|
})
|
|
us := users
|
|
rand.Shuffle(len(us), func(i, j int) {
|
|
us[i], us[j] = us[j], us[i]
|
|
})
|
|
got := filterNodesByUser(ns, us, test.args.user)
|
|
sort.Slice(got, func(i, j int) bool {
|
|
return got[i].ID < got[j].ID
|
|
})
|
|
|
|
if diff := cmp.Diff(test.want, got, util.Comparers...); diff != "" {
|
|
t.Errorf("filterNodesByUser() = (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_expandAlias(t *testing.T) {
|
|
set := func(ips []string, prefixes []string) *netipx.IPSet {
|
|
var builder netipx.IPSetBuilder
|
|
|
|
for _, ip := range ips {
|
|
builder.Add(netip.MustParseAddr(ip))
|
|
}
|
|
|
|
for _, pre := range prefixes {
|
|
builder.AddPrefix(netip.MustParsePrefix(pre))
|
|
}
|
|
|
|
s, _ := builder.IPSet()
|
|
|
|
return s
|
|
}
|
|
|
|
users := []types.User{
|
|
{Model: gorm.Model{ID: 1}, Name: "joe"},
|
|
{Model: gorm.Model{ID: 2}, Name: "marc"},
|
|
{Model: gorm.Model{ID: 3}, Name: "mickael"},
|
|
}
|
|
|
|
type field struct {
|
|
pol ACLPolicy
|
|
}
|
|
type args struct {
|
|
nodes types.Nodes
|
|
aclPolicy ACLPolicy
|
|
alias string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
field field
|
|
args args
|
|
want *netipx.IPSet
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "wildcard",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "*",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.78.84.227"),
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{}, []string{
|
|
"0.0.0.0/0",
|
|
"::/0",
|
|
}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple group",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "group:accountant",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: users[0],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: users[0],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.3"),
|
|
User: users[1],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: users[2],
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{
|
|
"100.64.0.1", "100.64.0.2", "100.64.0.3",
|
|
}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wrong group",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "group:hr",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: users[0],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: users[0],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.3"),
|
|
User: users[1],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: users[2],
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{}, []string{}),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "simple ipaddress",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "10.0.0.3",
|
|
nodes: types.Nodes{},
|
|
},
|
|
want: set([]string{
|
|
"10.0.0.3",
|
|
}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple host by ip passed through",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "10.0.0.1",
|
|
nodes: types.Nodes{},
|
|
},
|
|
want: set([]string{
|
|
"10.0.0.1",
|
|
}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple host by ipv4 single ipv4",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "10.0.0.1",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("10.0.0.1"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{
|
|
"10.0.0.1",
|
|
}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple host by ipv4 single dual stack",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "10.0.0.1",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("10.0.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{
|
|
"10.0.0.1", "fd7a:115c:a1e0:ab12:4843:2222:6273:2222",
|
|
}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple host by ipv6 single dual stack",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("10.0.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{
|
|
"fd7a:115c:a1e0:ab12:4843:2222:6273:2222", "10.0.0.1",
|
|
}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple host by hostname alias",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
"testy": netip.MustParsePrefix("10.0.0.132/32"),
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "testy",
|
|
nodes: types.Nodes{},
|
|
},
|
|
want: set([]string{}, []string{"10.0.0.132/32"}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "private network",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
"homeNetwork": netip.MustParsePrefix("192.168.1.0/24"),
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "homeNetwork",
|
|
nodes: types.Nodes{},
|
|
},
|
|
want: set([]string{}, []string{"192.168.1.0/24"}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple CIDR",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "10.0.0.0/16",
|
|
nodes: types.Nodes{},
|
|
aclPolicy: ACLPolicy{},
|
|
},
|
|
want: set([]string{}, []string{"10.0.0.0/16"}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple tag",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "tag:hr-webserver",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: users[0],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:hr-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: users[0],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:hr-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.3"),
|
|
User: users[1],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: users[0],
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{
|
|
"100.64.0.1", "100.64.0.2",
|
|
}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "No tag defined",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
|
|
TagOwners: TagOwners{
|
|
"tag:accountant-webserver": []string{"group:accountant"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "tag:hr-webserver",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{}, []string{}),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Forced tag defined",
|
|
field: field{
|
|
pol: ACLPolicy{},
|
|
},
|
|
args: args{
|
|
alias: "tag:hr-webserver",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: users[0],
|
|
ForcedTags: []string{"tag:hr-webserver"},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: users[0],
|
|
ForcedTags: []string{"tag:hr-webserver"},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.3"),
|
|
User: users[1],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: users[2],
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{"100.64.0.1", "100.64.0.2"}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Forced tag with legitimate tagOwner",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
TagOwners: TagOwners{
|
|
"tag:hr-webserver": []string{"joe"},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "tag:hr-webserver",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: users[0],
|
|
ForcedTags: []string{"tag:hr-webserver"},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: users[0],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:hr-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.3"),
|
|
User: users[1],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: users[2],
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{"100.64.0.1", "100.64.0.2"}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "list host in user without correctly tagged servers",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
|
|
},
|
|
},
|
|
args: args{
|
|
alias: "joe",
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:accountant-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:accountant-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.3"),
|
|
User: users[1],
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: users[0],
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
},
|
|
want: set([]string{"100.64.0.4"}, []string{}),
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got, err := test.field.pol.ExpandAlias(
|
|
test.args.nodes,
|
|
users,
|
|
test.args.alias,
|
|
)
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)
|
|
|
|
return
|
|
}
|
|
if diff := cmp.Diff(test.want, got); diff != "" {
|
|
t.Errorf("expandAlias() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
|
type args struct {
|
|
aclPolicy *ACLPolicy
|
|
nodes types.Nodes
|
|
user string
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want types.Nodes
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "exclude nodes with valid tags",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
|
|
},
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:accountant-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:accountant-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
user: "joe",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "exclude nodes with valid tags, and owner is in a group",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
Groups: Groups{
|
|
"group:accountant": []string{"joe", "bar"},
|
|
},
|
|
TagOwners: TagOwners{
|
|
"tag:accountant-webserver": []string{"group:accountant"},
|
|
},
|
|
},
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:accountant-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:accountant-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
user: "joe",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "exclude nodes with valid tags and with forced tags",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
|
|
},
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "foo",
|
|
RequestTags: []string{"tag:accountant-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "joe"},
|
|
ForcedTags: []string{"tag:accountant-webserver"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
user: "joe",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "all nodes have invalid tags, don't exclude them",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
|
|
},
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "hr-web1",
|
|
RequestTags: []string{"tag:hr-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "hr-web2",
|
|
RequestTags: []string{"tag:hr-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
user: "joe",
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "hr-web1",
|
|
RequestTags: []string{"tag:hr-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "hr-web2",
|
|
RequestTags: []string{"tag:hr-webserver"},
|
|
},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.4"),
|
|
User: types.User{Name: "joe"},
|
|
Hostinfo: &tailcfg.Hostinfo{},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got := excludeCorrectlyTaggedNodes(
|
|
test.args.aclPolicy,
|
|
test.args.nodes,
|
|
test.args.user,
|
|
)
|
|
if diff := cmp.Diff(test.want, got, util.Comparers...); diff != "" {
|
|
t.Errorf("excludeCorrectlyTaggedNodes() (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestACLPolicy_generateFilterRules(t *testing.T) {
|
|
type field struct {
|
|
pol ACLPolicy
|
|
}
|
|
type args struct {
|
|
nodes types.Nodes
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
field field
|
|
args args
|
|
want []tailcfg.FilterRule
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "no-policy",
|
|
field: field{},
|
|
args: args{},
|
|
want: nil,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "allow-all",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
|
|
},
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "0.0.0.0/0",
|
|
Ports: tailcfg.PortRange{
|
|
First: 0,
|
|
Last: 65535,
|
|
},
|
|
},
|
|
{
|
|
IP: "::/0",
|
|
Ports: tailcfg.PortRange{
|
|
First: 0,
|
|
Last: 65535,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "host1-can-reach-host2-full",
|
|
field: field{
|
|
pol: ACLPolicy{
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"100.64.0.2"},
|
|
Destinations: []string{"100.64.0.1:*"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
args: args{
|
|
nodes: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0:ab12:4843:2222:6273:2222/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.64.0.1/32",
|
|
Ports: tailcfg.PortRange{
|
|
First: 0,
|
|
Last: 65535,
|
|
},
|
|
},
|
|
{
|
|
IP: "fd7a:115c:a1e0:ab12:4843:2222:6273:2221/128",
|
|
Ports: tailcfg.PortRange{
|
|
First: 0,
|
|
Last: 65535,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tt.field.pol.CompileFilterRules(
|
|
[]types.User{},
|
|
tt.args.nodes,
|
|
)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ACLgenerateFilterRules() error = %v, wantErr %v", err, tt.wantErr)
|
|
|
|
return
|
|
}
|
|
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
log.Trace().Interface("got", got).Msg("result")
|
|
t.Errorf("ACLgenerateFilterRules() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// tsExitNodeDest is the list of destination IP ranges that are allowed when
|
|
// you dump the filter list from a Tailscale node connected to Tailscale SaaS.
|
|
var tsExitNodeDest = []tailcfg.NetPortRange{
|
|
{
|
|
IP: "0.0.0.0-9.255.255.255",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "11.0.0.0-100.63.255.255",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "100.128.0.0-169.253.255.255",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "169.255.0.0-172.15.255.255",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "172.32.0.0-192.167.255.255",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "192.169.0.0-255.255.255.255",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "2000::-3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
}
|
|
|
|
// hsExitNodeDest is the list of destination IP ranges that are allowed when
|
|
// we use headscale "autogroup:internet".
|
|
var hsExitNodeDest = []tailcfg.NetPortRange{
|
|
{IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
|
{IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny},
|
|
{IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny},
|
|
{IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny},
|
|
{IP: "64.0.0.0/3", Ports: tailcfg.PortRangeAny},
|
|
{IP: "96.0.0.0/6", Ports: tailcfg.PortRangeAny},
|
|
{IP: "100.0.0.0/10", Ports: tailcfg.PortRangeAny},
|
|
{IP: "100.128.0.0/9", Ports: tailcfg.PortRangeAny},
|
|
{IP: "101.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "102.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "104.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
|
{IP: "112.0.0.0/4", Ports: tailcfg.PortRangeAny},
|
|
{IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny},
|
|
{IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
|
{IP: "168.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.0.0.0/9", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.128.0.0/10", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.192.0.0/11", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.224.0.0/12", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.240.0.0/13", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.248.0.0/14", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.252.0.0/15", Ports: tailcfg.PortRangeAny},
|
|
{IP: "169.255.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
{IP: "170.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny},
|
|
{IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny},
|
|
{IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny},
|
|
{IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
|
{IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny},
|
|
{IP: "224.0.0.0/3", Ports: tailcfg.PortRangeAny},
|
|
{IP: "2000::/3", Ports: tailcfg.PortRangeAny},
|
|
}
|
|
|
|
func TestTheInternet(t *testing.T) {
|
|
internetSet := theInternet()
|
|
|
|
internetPrefs := internetSet.Prefixes()
|
|
|
|
for i := range internetPrefs {
|
|
if internetPrefs[i].String() != hsExitNodeDest[i].IP {
|
|
t.Errorf(
|
|
"prefix from internet set %q != hsExit list %q",
|
|
internetPrefs[i].String(),
|
|
hsExitNodeDest[i].IP,
|
|
)
|
|
}
|
|
}
|
|
|
|
if len(internetPrefs) != len(hsExitNodeDest) {
|
|
t.Fatalf(
|
|
"expected same length of prefixes, internet: %d, hsExit: %d",
|
|
len(internetPrefs),
|
|
len(hsExitNodeDest),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestReduceFilterRules(t *testing.T) {
|
|
users := []types.User{
|
|
{Model: gorm.Model{ID: 1}, Name: "mickael"},
|
|
{Model: gorm.Model{ID: 2}, Name: "user1"},
|
|
{Model: gorm.Model{ID: 3}, Name: "user2"},
|
|
{Model: gorm.Model{ID: 4}, Name: "user100"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
node *types.Node
|
|
peers types.Nodes
|
|
pol ACLPolicy
|
|
want []tailcfg.FilterRule
|
|
}{
|
|
{
|
|
name: "host1-can-reach-host2-no-rules",
|
|
pol: ACLPolicy{
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"100.64.0.1"},
|
|
Destinations: []string{"100.64.0.2:*"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
|
|
User: users[0],
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
|
|
User: users[0],
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{},
|
|
},
|
|
{
|
|
name: "1604-subnet-routers-are-preserved",
|
|
pol: ACLPolicy{
|
|
Groups: Groups{
|
|
"group:admins": {"user1"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:admins"},
|
|
Destinations: []string{"group:admins:*"},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:admins"},
|
|
Destinations: []string{"10.33.0.0/16:*"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: users[1],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RoutableIPs: []netip.Prefix{
|
|
netip.MustParsePrefix("10.33.0.0/16"),
|
|
},
|
|
},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0::2"),
|
|
User: users[1],
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.64.0.1/32",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "fd7a:115c:a1e0::1/128",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "10.33.0.0/16",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "1786-reducing-breaks-exit-nodes-the-client",
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
// Exit node
|
|
"internal": netip.MustParsePrefix("100.64.0.100/32"),
|
|
},
|
|
Groups: Groups{
|
|
"group:team": {"user3", "user2", "user1"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"internal:*",
|
|
},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"autogroup:internet:*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: users[1],
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0::2"),
|
|
User: users[2],
|
|
},
|
|
// "internal" exit node
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.100"),
|
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
|
User: users[3],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RoutableIPs: tsaddr.ExitRoutes(),
|
|
},
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{},
|
|
},
|
|
{
|
|
name: "1786-reducing-breaks-exit-nodes-the-exit",
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
// Exit node
|
|
"internal": netip.MustParsePrefix("100.64.0.100/32"),
|
|
},
|
|
Groups: Groups{
|
|
"group:team": {"user3", "user2", "user1"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"internal:*",
|
|
},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"autogroup:internet:*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.100"),
|
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
|
User: types.User{Name: "user100"},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RoutableIPs: tsaddr.ExitRoutes(),
|
|
},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0::2"),
|
|
User: users[2],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: users[1],
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.64.0.100/32",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "fd7a:115c:a1e0::100/128",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: hsExitNodeDest,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "1786-reducing-breaks-exit-nodes-the-example-from-issue",
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
// Exit node
|
|
"internal": netip.MustParsePrefix("100.64.0.100/32"),
|
|
},
|
|
Groups: Groups{
|
|
"group:team": {"user3", "user2", "user1"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"internal:*",
|
|
},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"0.0.0.0/5:*",
|
|
"8.0.0.0/7:*",
|
|
"11.0.0.0/8:*",
|
|
"12.0.0.0/6:*",
|
|
"16.0.0.0/4:*",
|
|
"32.0.0.0/3:*",
|
|
"64.0.0.0/2:*",
|
|
"128.0.0.0/3:*",
|
|
"160.0.0.0/5:*",
|
|
"168.0.0.0/6:*",
|
|
"172.0.0.0/12:*",
|
|
"172.32.0.0/11:*",
|
|
"172.64.0.0/10:*",
|
|
"172.128.0.0/9:*",
|
|
"173.0.0.0/8:*",
|
|
"174.0.0.0/7:*",
|
|
"176.0.0.0/4:*",
|
|
"192.0.0.0/9:*",
|
|
"192.128.0.0/11:*",
|
|
"192.160.0.0/13:*",
|
|
"192.169.0.0/16:*",
|
|
"192.170.0.0/15:*",
|
|
"192.172.0.0/14:*",
|
|
"192.176.0.0/12:*",
|
|
"192.192.0.0/10:*",
|
|
"193.0.0.0/8:*",
|
|
"194.0.0.0/7:*",
|
|
"196.0.0.0/6:*",
|
|
"200.0.0.0/5:*",
|
|
"208.0.0.0/4:*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.100"),
|
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
|
User: users[3],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RoutableIPs: tsaddr.ExitRoutes(),
|
|
},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0::2"),
|
|
User: users[2],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: users[1],
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.64.0.100/32",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "fd7a:115c:a1e0::100/128",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "0.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
|
{IP: "8.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "11.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "12.0.0.0/6", Ports: tailcfg.PortRangeAny},
|
|
{IP: "16.0.0.0/4", Ports: tailcfg.PortRangeAny},
|
|
{IP: "32.0.0.0/3", Ports: tailcfg.PortRangeAny},
|
|
{IP: "64.0.0.0/2", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::100/128", Ports: tailcfg.PortRangeAny},
|
|
{IP: "128.0.0.0/3", Ports: tailcfg.PortRangeAny},
|
|
{IP: "160.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
|
{IP: "168.0.0.0/6", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.0.0.0/12", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.32.0.0/11", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.64.0.0/10", Ports: tailcfg.PortRangeAny},
|
|
{IP: "172.128.0.0/9", Ports: tailcfg.PortRangeAny},
|
|
{IP: "173.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "174.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "176.0.0.0/4", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.0.0.0/9", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.128.0.0/11", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.160.0.0/13", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.169.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.170.0.0/15", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.172.0.0/14", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.176.0.0/12", Ports: tailcfg.PortRangeAny},
|
|
{IP: "192.192.0.0/10", Ports: tailcfg.PortRangeAny},
|
|
{IP: "193.0.0.0/8", Ports: tailcfg.PortRangeAny},
|
|
{IP: "194.0.0.0/7", Ports: tailcfg.PortRangeAny},
|
|
{IP: "196.0.0.0/6", Ports: tailcfg.PortRangeAny},
|
|
{IP: "200.0.0.0/5", Ports: tailcfg.PortRangeAny},
|
|
{IP: "208.0.0.0/4", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "1786-reducing-breaks-exit-nodes-app-connector-like",
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
// Exit node
|
|
"internal": netip.MustParsePrefix("100.64.0.100/32"),
|
|
},
|
|
Groups: Groups{
|
|
"group:team": {"user3", "user2", "user1"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"internal:*",
|
|
},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"8.0.0.0/8:*",
|
|
"16.0.0.0/8:*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.100"),
|
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
|
User: users[3],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RoutableIPs: []netip.Prefix{
|
|
netip.MustParsePrefix("8.0.0.0/16"),
|
|
netip.MustParsePrefix("16.0.0.0/16"),
|
|
},
|
|
},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0::2"),
|
|
User: users[2],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: users[1],
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.64.0.100/32",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "fd7a:115c:a1e0::100/128",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "8.0.0.0/8",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "16.0.0.0/8",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "1786-reducing-breaks-exit-nodes-app-connector-like2",
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
// Exit node
|
|
"internal": netip.MustParsePrefix("100.64.0.100/32"),
|
|
},
|
|
Groups: Groups{
|
|
"group:team": {"user3", "user2", "user1"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"internal:*",
|
|
},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:team"},
|
|
Destinations: []string{
|
|
"8.0.0.0/16:*",
|
|
"16.0.0.0/16:*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.100"),
|
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
|
User: users[3],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RoutableIPs: []netip.Prefix{
|
|
netip.MustParsePrefix("8.0.0.0/8"),
|
|
netip.MustParsePrefix("16.0.0.0/8"),
|
|
},
|
|
},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0::2"),
|
|
User: users[2],
|
|
},
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: users[1],
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.64.0.100/32",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "fd7a:115c:a1e0::100/128",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "8.0.0.0/16",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "16.0.0.0/16",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "1817-reduce-breaks-32-mask",
|
|
pol: ACLPolicy{
|
|
Hosts: Hosts{
|
|
"vlan1": netip.MustParsePrefix("172.16.0.0/24"),
|
|
"dns1": netip.MustParsePrefix("172.16.0.21/32"),
|
|
},
|
|
Groups: Groups{
|
|
"group:access": {"user1"},
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:access"},
|
|
Destinations: []string{
|
|
"tag:access-servers:*",
|
|
"dns1:*",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
IPv4: iap("100.64.0.100"),
|
|
IPv6: iap("fd7a:115c:a1e0::100"),
|
|
User: users[3],
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RoutableIPs: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/24")},
|
|
},
|
|
ForcedTags: []string{"tag:access-servers"},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: users[1],
|
|
},
|
|
},
|
|
want: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"100.64.0.1/32", "fd7a:115c:a1e0::1/128"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "100.64.0.100/32",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "fd7a:115c:a1e0::100/128",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
{
|
|
IP: "172.16.0.21/32",
|
|
Ports: tailcfg.PortRangeAny,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, _ := tt.pol.CompileFilterRules(
|
|
users,
|
|
append(tt.peers, tt.node),
|
|
)
|
|
|
|
got = ReduceFilterRules(tt.node, got)
|
|
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
log.Trace().Interface("got", got).Msg("result")
|
|
t.Errorf("TestReduceFilterRules() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getTags(t *testing.T) {
|
|
type args struct {
|
|
aclPolicy *ACLPolicy
|
|
node *types.Node
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantInvalid []string
|
|
wantValid []string
|
|
}{
|
|
{
|
|
name: "valid tag one nodes",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{
|
|
"tag:valid": []string{"joe"},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
User: types.User{
|
|
Name: "joe",
|
|
},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RequestTags: []string{"tag:valid"},
|
|
},
|
|
},
|
|
},
|
|
wantValid: []string{"tag:valid"},
|
|
wantInvalid: nil,
|
|
},
|
|
{
|
|
name: "invalid tag and valid tag one nodes",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{
|
|
"tag:valid": []string{"joe"},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
User: types.User{
|
|
Name: "joe",
|
|
},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RequestTags: []string{"tag:valid", "tag:invalid"},
|
|
},
|
|
},
|
|
},
|
|
wantValid: []string{"tag:valid"},
|
|
wantInvalid: []string{"tag:invalid"},
|
|
},
|
|
{
|
|
name: "multiple invalid and identical tags, should return only one invalid tag",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{
|
|
"tag:valid": []string{"joe"},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
User: types.User{
|
|
Name: "joe",
|
|
},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RequestTags: []string{
|
|
"tag:invalid",
|
|
"tag:valid",
|
|
"tag:invalid",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantValid: []string{"tag:valid"},
|
|
wantInvalid: []string{"tag:invalid"},
|
|
},
|
|
{
|
|
name: "only invalid tags",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{
|
|
TagOwners: TagOwners{
|
|
"tag:valid": []string{"joe"},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
User: types.User{
|
|
Name: "joe",
|
|
},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RequestTags: []string{"tag:invalid", "very-invalid"},
|
|
},
|
|
},
|
|
},
|
|
wantValid: nil,
|
|
wantInvalid: []string{"tag:invalid", "very-invalid"},
|
|
},
|
|
{
|
|
name: "empty ACLPolicy should return empty tags and should not panic",
|
|
args: args{
|
|
aclPolicy: &ACLPolicy{},
|
|
node: &types.Node{
|
|
User: types.User{
|
|
Name: "joe",
|
|
},
|
|
Hostinfo: &tailcfg.Hostinfo{
|
|
RequestTags: []string{"tag:invalid", "very-invalid"},
|
|
},
|
|
},
|
|
},
|
|
wantValid: nil,
|
|
wantInvalid: []string{"tag:invalid", "very-invalid"},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
gotValid, gotInvalid := test.args.aclPolicy.TagsOfNode(
|
|
test.args.node,
|
|
)
|
|
for _, valid := range gotValid {
|
|
if !slices.Contains(test.wantValid, valid) {
|
|
t.Errorf(
|
|
"valids: getTags() = %v, want %v",
|
|
gotValid,
|
|
test.wantValid,
|
|
)
|
|
|
|
break
|
|
}
|
|
}
|
|
for _, invalid := range gotInvalid {
|
|
if !slices.Contains(test.wantInvalid, invalid) {
|
|
t.Errorf(
|
|
"invalids: getTags() = %v, want %v",
|
|
gotInvalid,
|
|
test.wantInvalid,
|
|
)
|
|
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getFilteredByACLPeers(t *testing.T) {
|
|
type args struct {
|
|
nodes types.Nodes
|
|
rules []tailcfg.FilterRule
|
|
node *types.Node
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want types.Nodes
|
|
}{
|
|
{
|
|
name: "all hosts can talk to each other",
|
|
args: args{
|
|
nodes: types.Nodes{ // list of all nodes in the database
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
{
|
|
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "*"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "One host can talk to another, but not all hosts",
|
|
args: args{
|
|
nodes: types.Nodes{ // list of all nodes in the database
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
{
|
|
SrcIPs: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.2"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "host cannot directly talk to destination, but return path is authorized",
|
|
args: args{
|
|
nodes: types.Nodes{ // list of all nodes in the database
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
{
|
|
SrcIPs: []string{"100.64.0.3"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.2"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "rules allows all hosts to reach one destination",
|
|
args: args{
|
|
nodes: types.Nodes{ // list of all nodes in the database
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
{
|
|
SrcIPs: []string{"*"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.2"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "rules allows all hosts to reach one destination, destination can reach all hosts",
|
|
args: args{
|
|
nodes: types.Nodes{ // list of all nodes in the database
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
{
|
|
SrcIPs: []string{"*"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.2"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "rule allows all hosts to reach all destinations",
|
|
args: args{
|
|
nodes: types.Nodes{ // list of all nodes in the database
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
{
|
|
SrcIPs: []string{"*"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "*"},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "without rule all communications are forbidden",
|
|
args: args{
|
|
nodes: types.Nodes{ // list of all nodes in the database
|
|
&types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
User: types.User{Name: "joe"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.3"),
|
|
User: types.User{Name: "mickael"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
User: types.User{Name: "marc"},
|
|
},
|
|
},
|
|
want: nil,
|
|
},
|
|
{
|
|
// Investigating 699
|
|
// Found some nodes: [ts-head-8w6paa ts-unstable-lys2ib ts-head-upcrmb ts-unstable-rlwpvr] nodes=ts-head-8w6paa
|
|
// ACL rules generated ACL=[{"DstPorts":[{"Bits":null,"IP":"*","Ports":{"First":0,"Last":65535}}],"SrcIPs":["fd7a:115c:a1e0::3","100.64.0.3","fd7a:115c:a1e0::4","100.64.0.4"]}]
|
|
// ACL Cache Map={"100.64.0.3":{"*":{}},"100.64.0.4":{"*":{}},"fd7a:115c:a1e0::3":{"*":{}},"fd7a:115c:a1e0::4":{"*":{}}}
|
|
name: "issue-699-broken-star",
|
|
args: args{
|
|
nodes: types.Nodes{ //
|
|
&types.Node{
|
|
ID: 1,
|
|
Hostname: "ts-head-upcrmb",
|
|
IPv4: iap("100.64.0.3"),
|
|
IPv6: iap("fd7a:115c:a1e0::3"),
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
Hostname: "ts-unstable-rlwpvr",
|
|
IPv4: iap("100.64.0.4"),
|
|
IPv6: iap("fd7a:115c:a1e0::4"),
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
&types.Node{
|
|
ID: 3,
|
|
Hostname: "ts-head-8w6paa",
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
&types.Node{
|
|
ID: 4,
|
|
Hostname: "ts-unstable-lys2ib",
|
|
IPv4: iap("100.64.0.2"),
|
|
IPv6: iap("fd7a:115c:a1e0::2"),
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
|
|
{
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{
|
|
IP: "*",
|
|
Ports: tailcfg.PortRange{First: 0, Last: 65535},
|
|
},
|
|
},
|
|
SrcIPs: []string{
|
|
"fd7a:115c:a1e0::3", "100.64.0.3",
|
|
"fd7a:115c:a1e0::4", "100.64.0.4",
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{ // current nodes
|
|
ID: 3,
|
|
Hostname: "ts-head-8w6paa",
|
|
IPv4: iap("100.64.0.1"),
|
|
IPv6: iap("fd7a:115c:a1e0::1"),
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
},
|
|
want: types.Nodes{
|
|
&types.Node{
|
|
ID: 1,
|
|
Hostname: "ts-head-upcrmb",
|
|
IPv4: iap("100.64.0.3"),
|
|
IPv6: iap("fd7a:115c:a1e0::3"),
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
&types.Node{
|
|
ID: 2,
|
|
Hostname: "ts-unstable-rlwpvr",
|
|
IPv4: iap("100.64.0.4"),
|
|
IPv6: iap("fd7a:115c:a1e0::4"),
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "failing-edge-case-during-p3-refactor",
|
|
args: args{
|
|
nodes: []*types.Node{
|
|
{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.2"),
|
|
Hostname: "peer1",
|
|
User: types.User{Name: "mini"},
|
|
},
|
|
{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.3"),
|
|
Hostname: "peer2",
|
|
User: types.User{Name: "peer2"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"100.64.0.1/32"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "::/0", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
ID: 0,
|
|
IPv4: iap("100.64.0.1"),
|
|
Hostname: "mini",
|
|
User: types.User{Name: "mini"},
|
|
},
|
|
},
|
|
want: []*types.Node{
|
|
{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.3"),
|
|
Hostname: "peer2",
|
|
User: types.User{Name: "peer2"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "p4-host-in-netmap-user2-dest-bug",
|
|
args: args{
|
|
nodes: []*types.Node{
|
|
{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.2"),
|
|
Hostname: "user1-2",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
{
|
|
ID: 0,
|
|
IPv4: iap("100.64.0.1"),
|
|
Hostname: "user1-1",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.4"),
|
|
Hostname: "user2-2",
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.3/32",
|
|
"100.64.0.4/32",
|
|
"fd7a:115c:a1e0::3/128",
|
|
"fd7a:115c:a1e0::4/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.3"),
|
|
Hostname: "user-2-1",
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
},
|
|
want: []*types.Node{
|
|
{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.2"),
|
|
Hostname: "user1-2",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
{
|
|
ID: 0,
|
|
IPv4: iap("100.64.0.1"),
|
|
Hostname: "user1-1",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.4"),
|
|
Hostname: "user2-2",
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "p4-host-in-netmap-user1-dest-bug",
|
|
args: args{
|
|
nodes: []*types.Node{
|
|
{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.2"),
|
|
Hostname: "user1-2",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.3"),
|
|
Hostname: "user-2-1",
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.4"),
|
|
Hostname: "user2-2",
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.1/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "100.64.0.2/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::1/128", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::2/128", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
"100.64.0.2/32",
|
|
"fd7a:115c:a1e0::1/128",
|
|
"fd7a:115c:a1e0::2/128",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.3/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "100.64.0.4/32", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::3/128", Ports: tailcfg.PortRangeAny},
|
|
{IP: "fd7a:115c:a1e0::4/128", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
ID: 0,
|
|
IPv4: iap("100.64.0.1"),
|
|
Hostname: "user1-1",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
},
|
|
want: []*types.Node{
|
|
{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.2"),
|
|
Hostname: "user1-2",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.3"),
|
|
Hostname: "user-2-1",
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
{
|
|
ID: 3,
|
|
IPv4: iap("100.64.0.4"),
|
|
Hostname: "user2-2",
|
|
User: types.User{Name: "user2"},
|
|
},
|
|
},
|
|
},
|
|
|
|
{
|
|
name: "subnet-router-with-only-route",
|
|
args: args{
|
|
nodes: []*types.Node{
|
|
{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
Hostname: "user1",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
Hostname: "router",
|
|
User: types.User{Name: "router"},
|
|
Routes: types.Routes{
|
|
types.Route{
|
|
NodeID: 2,
|
|
Prefix: netip.MustParsePrefix("10.33.0.0/16"),
|
|
IsPrimary: true,
|
|
Enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
rules: []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{
|
|
"100.64.0.1/32",
|
|
},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "10.33.0.0/16", Ports: tailcfg.PortRangeAny},
|
|
},
|
|
},
|
|
},
|
|
node: &types.Node{
|
|
ID: 1,
|
|
IPv4: iap("100.64.0.1"),
|
|
Hostname: "user1",
|
|
User: types.User{Name: "user1"},
|
|
},
|
|
},
|
|
want: []*types.Node{
|
|
{
|
|
ID: 2,
|
|
IPv4: iap("100.64.0.2"),
|
|
Hostname: "router",
|
|
User: types.User{Name: "router"},
|
|
Routes: types.Routes{
|
|
types.Route{
|
|
NodeID: 2,
|
|
Prefix: netip.MustParsePrefix("10.33.0.0/16"),
|
|
IsPrimary: true,
|
|
Enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := FilterNodesByACL(
|
|
tt.args.node,
|
|
tt.args.nodes,
|
|
tt.args.rules,
|
|
)
|
|
if diff := cmp.Diff(tt.want, got, util.Comparers...); diff != "" {
|
|
t.Errorf("FilterNodesByACL() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSSHRules(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
node types.Node
|
|
peers types.Nodes
|
|
pol ACLPolicy
|
|
want *tailcfg.SSHPolicy
|
|
}{
|
|
{
|
|
name: "peers-can-connect",
|
|
node: types.Node{
|
|
Hostname: "testnodes",
|
|
IPv4: iap("100.64.99.42"),
|
|
UserID: 0,
|
|
User: types.User{
|
|
Name: "user1",
|
|
},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
Hostname: "testnodes2",
|
|
IPv4: iap("100.64.0.1"),
|
|
UserID: 0,
|
|
User: types.User{
|
|
Name: "user1",
|
|
},
|
|
},
|
|
},
|
|
pol: ACLPolicy{
|
|
Groups: Groups{
|
|
"group:test": []string{"user1"},
|
|
},
|
|
Hosts: Hosts{
|
|
"client": netip.PrefixFrom(netip.MustParseAddr("100.64.99.42"), 32),
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
SSHs: []SSH{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:test"},
|
|
Destinations: []string{"client"},
|
|
Users: []string{"autogroup:nonroot"},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"client"},
|
|
Users: []string{"autogroup:nonroot"},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:test"},
|
|
Destinations: []string{"100.64.99.42"},
|
|
Users: []string{"autogroup:nonroot"},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"100.64.99.42"},
|
|
Users: []string{"autogroup:nonroot"},
|
|
},
|
|
},
|
|
},
|
|
want: &tailcfg.SSHPolicy{Rules: []*tailcfg.SSHRule{
|
|
{
|
|
Principals: []*tailcfg.SSHPrincipal{
|
|
{
|
|
UserLogin: "user1",
|
|
},
|
|
},
|
|
SSHUsers: map[string]string{
|
|
"autogroup:nonroot": "=",
|
|
},
|
|
Action: &tailcfg.SSHAction{
|
|
Accept: true,
|
|
AllowAgentForwarding: true,
|
|
AllowLocalPortForwarding: true,
|
|
},
|
|
},
|
|
{
|
|
SSHUsers: map[string]string{
|
|
"autogroup:nonroot": "=",
|
|
},
|
|
Principals: []*tailcfg.SSHPrincipal{
|
|
{
|
|
Any: true,
|
|
},
|
|
},
|
|
Action: &tailcfg.SSHAction{
|
|
Accept: true,
|
|
AllowAgentForwarding: true,
|
|
AllowLocalPortForwarding: true,
|
|
},
|
|
},
|
|
{
|
|
Principals: []*tailcfg.SSHPrincipal{
|
|
{
|
|
UserLogin: "user1",
|
|
},
|
|
},
|
|
SSHUsers: map[string]string{
|
|
"autogroup:nonroot": "=",
|
|
},
|
|
Action: &tailcfg.SSHAction{
|
|
Accept: true,
|
|
AllowAgentForwarding: true,
|
|
AllowLocalPortForwarding: true,
|
|
},
|
|
},
|
|
{
|
|
SSHUsers: map[string]string{
|
|
"autogroup:nonroot": "=",
|
|
},
|
|
Principals: []*tailcfg.SSHPrincipal{
|
|
{
|
|
Any: true,
|
|
},
|
|
},
|
|
Action: &tailcfg.SSHAction{
|
|
Accept: true,
|
|
AllowAgentForwarding: true,
|
|
AllowLocalPortForwarding: true,
|
|
},
|
|
},
|
|
}},
|
|
},
|
|
{
|
|
name: "peers-cannot-connect",
|
|
node: types.Node{
|
|
Hostname: "testnodes",
|
|
IPv4: iap("100.64.0.1"),
|
|
UserID: 0,
|
|
User: types.User{
|
|
Name: "user1",
|
|
},
|
|
},
|
|
peers: types.Nodes{
|
|
&types.Node{
|
|
Hostname: "testnodes2",
|
|
IPv4: iap("100.64.99.42"),
|
|
UserID: 0,
|
|
User: types.User{
|
|
Name: "user1",
|
|
},
|
|
},
|
|
},
|
|
pol: ACLPolicy{
|
|
Groups: Groups{
|
|
"group:test": []string{"user1"},
|
|
},
|
|
Hosts: Hosts{
|
|
"client": netip.PrefixFrom(netip.MustParseAddr("100.64.99.42"), 32),
|
|
},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
SSHs: []SSH{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"group:test"},
|
|
Destinations: []string{"100.64.99.42"},
|
|
Users: []string{"autogroup:nonroot"},
|
|
},
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"100.64.99.42"},
|
|
Users: []string{"autogroup:nonroot"},
|
|
},
|
|
},
|
|
},
|
|
want: &tailcfg.SSHPolicy{Rules: nil},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := tt.pol.CompileSSHPolicy(&tt.node, []types.User{}, tt.peers)
|
|
require.NoError(t, err)
|
|
|
|
if diff := cmp.Diff(tt.want, got); diff != "" {
|
|
t.Errorf("TestSSHRules() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseDestination(t *testing.T) {
|
|
tests := []struct {
|
|
dest string
|
|
wantAlias string
|
|
wantPort string
|
|
}{
|
|
{
|
|
dest: "git-server:*",
|
|
wantAlias: "git-server",
|
|
wantPort: "*",
|
|
},
|
|
{
|
|
dest: "192.168.1.0/24:22",
|
|
wantAlias: "192.168.1.0/24",
|
|
wantPort: "22",
|
|
},
|
|
{
|
|
dest: "192.168.1.1:22",
|
|
wantAlias: "192.168.1.1",
|
|
wantPort: "22",
|
|
},
|
|
{
|
|
dest: "fd7a:115c:a1e0::2:22",
|
|
wantAlias: "fd7a:115c:a1e0::2",
|
|
wantPort: "22",
|
|
},
|
|
{
|
|
dest: "fd7a:115c:a1e0::2/128:22",
|
|
wantAlias: "fd7a:115c:a1e0::2/128",
|
|
wantPort: "22",
|
|
},
|
|
{
|
|
dest: "tag:montreal-webserver:80,443",
|
|
wantAlias: "tag:montreal-webserver",
|
|
wantPort: "80,443",
|
|
},
|
|
{
|
|
dest: "tag:api-server:443",
|
|
wantAlias: "tag:api-server",
|
|
wantPort: "443",
|
|
},
|
|
{
|
|
dest: "example-host-1:*",
|
|
wantAlias: "example-host-1",
|
|
wantPort: "*",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.dest, func(t *testing.T) {
|
|
alias, port, _ := parseDestination(tt.dest)
|
|
|
|
if alias != tt.wantAlias {
|
|
t.Errorf("unexpected alias: want(%s) != got(%s)", tt.wantAlias, alias)
|
|
}
|
|
|
|
if port != tt.wantPort {
|
|
t.Errorf("unexpected port: want(%s) != got(%s)", tt.wantPort, port)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// this test should validate that we can expand a group in a TagOWner section and
|
|
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
|
|
// the tag is matched in the Sources section.
|
|
func TestValidExpandTagOwnersInSources(t *testing.T) {
|
|
hostInfo := tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "testnodes",
|
|
RequestTags: []string{"tag:test"},
|
|
}
|
|
|
|
user := types.User{
|
|
Model: gorm.Model{ID: 1},
|
|
Name: "user1",
|
|
}
|
|
|
|
node := &types.Node{
|
|
ID: 0,
|
|
Hostname: "testnodes",
|
|
IPv4: iap("100.64.0.1"),
|
|
UserID: 0,
|
|
User: user,
|
|
RegisterMethod: util.RegisterMethodAuthKey,
|
|
Hostinfo: &hostInfo,
|
|
}
|
|
|
|
pol := &ACLPolicy{
|
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"tag:test"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, _, err := GenerateFilterAndSSHRulesForTests(pol, node, types.Nodes{}, []types.User{user})
|
|
require.NoError(t, err)
|
|
|
|
want := []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"100.64.0.1/32"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{Last: 65535}},
|
|
{IP: "::/0", Ports: tailcfg.PortRange{Last: 65535}},
|
|
},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("TestValidExpandTagOwnersInSources() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// need a test with:
|
|
// tag on a host that isn't owned by a tag owners. So the user
|
|
// of the host should be valid.
|
|
func TestInvalidTagValidUser(t *testing.T) {
|
|
hostInfo := tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "testnodes",
|
|
RequestTags: []string{"tag:foo"},
|
|
}
|
|
|
|
node := &types.Node{
|
|
ID: 1,
|
|
Hostname: "testnodes",
|
|
IPv4: iap("100.64.0.1"),
|
|
UserID: 1,
|
|
User: types.User{
|
|
Model: gorm.Model{ID: 1},
|
|
Name: "user1",
|
|
},
|
|
RegisterMethod: util.RegisterMethodAuthKey,
|
|
Hostinfo: &hostInfo,
|
|
}
|
|
|
|
pol := &ACLPolicy{
|
|
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"user1"},
|
|
Destinations: []string{"*:*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, _, err := GenerateFilterAndSSHRulesForTests(
|
|
pol,
|
|
node,
|
|
types.Nodes{},
|
|
[]types.User{node.User},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
want := []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"100.64.0.1/32"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "0.0.0.0/0", Ports: tailcfg.PortRange{Last: 65535}},
|
|
{IP: "::/0", Ports: tailcfg.PortRange{Last: 65535}},
|
|
},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("TestInvalidTagValidUser() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
// this test should validate that we can expand a group in a TagOWner section and
|
|
// match properly the IP's of the related hosts. The owner is valid and the tag is also valid.
|
|
// the tag is matched in the Destinations section.
|
|
func TestValidExpandTagOwnersInDestinations(t *testing.T) {
|
|
hostInfo := tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "testnodes",
|
|
RequestTags: []string{"tag:test"},
|
|
}
|
|
|
|
node := &types.Node{
|
|
ID: 1,
|
|
Hostname: "testnodes",
|
|
IPv4: iap("100.64.0.1"),
|
|
UserID: 1,
|
|
User: types.User{
|
|
Model: gorm.Model{ID: 1},
|
|
Name: "user1",
|
|
},
|
|
RegisterMethod: util.RegisterMethodAuthKey,
|
|
Hostinfo: &hostInfo,
|
|
}
|
|
|
|
pol := &ACLPolicy{
|
|
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
|
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"*"},
|
|
Destinations: []string{"tag:test:*"},
|
|
},
|
|
},
|
|
}
|
|
|
|
// rules, _, err := GenerateFilterRules(pol, &node, peers, false)
|
|
// c.Assert(err, check.IsNil)
|
|
//
|
|
// c.Assert(rules, check.HasLen, 1)
|
|
// c.Assert(rules[0].DstPorts, check.HasLen, 1)
|
|
// c.Assert(rules[0].DstPorts[0].IP, check.Equals, "100.64.0.1/32")
|
|
|
|
got, _, err := GenerateFilterAndSSHRulesForTests(
|
|
pol,
|
|
node,
|
|
types.Nodes{},
|
|
[]types.User{node.User},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
want := []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"0.0.0.0/0", "::/0"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.1/32", Ports: tailcfg.PortRange{Last: 65535}},
|
|
},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf(
|
|
"TestValidExpandTagOwnersInDestinations() unexpected result (-want +got):\n%s",
|
|
diff,
|
|
)
|
|
}
|
|
}
|
|
|
|
// tag on a host is owned by a tag owner, the tag is valid.
|
|
// an ACL rule is matching the tag to a user. It should not be valid since the
|
|
// host should be tied to the tag now.
|
|
func TestValidTagInvalidUser(t *testing.T) {
|
|
hostInfo := tailcfg.Hostinfo{
|
|
OS: "centos",
|
|
Hostname: "webserver",
|
|
RequestTags: []string{"tag:webapp"},
|
|
}
|
|
user := types.User{
|
|
Model: gorm.Model{ID: 1},
|
|
Name: "user1",
|
|
}
|
|
|
|
node := &types.Node{
|
|
ID: 1,
|
|
Hostname: "webserver",
|
|
IPv4: iap("100.64.0.1"),
|
|
UserID: 1,
|
|
User: user,
|
|
RegisterMethod: util.RegisterMethodAuthKey,
|
|
Hostinfo: &hostInfo,
|
|
}
|
|
|
|
hostInfo2 := tailcfg.Hostinfo{
|
|
OS: "debian",
|
|
Hostname: "Hostname",
|
|
}
|
|
|
|
nodes2 := &types.Node{
|
|
ID: 2,
|
|
Hostname: "user",
|
|
IPv4: iap("100.64.0.2"),
|
|
UserID: 1,
|
|
User: user,
|
|
RegisterMethod: util.RegisterMethodAuthKey,
|
|
Hostinfo: &hostInfo2,
|
|
}
|
|
|
|
pol := &ACLPolicy{
|
|
TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
|
|
ACLs: []ACL{
|
|
{
|
|
Action: "accept",
|
|
Sources: []string{"user1"},
|
|
Destinations: []string{"tag:webapp:80,443"},
|
|
},
|
|
},
|
|
}
|
|
|
|
got, _, err := GenerateFilterAndSSHRulesForTests(
|
|
pol,
|
|
node,
|
|
types.Nodes{nodes2},
|
|
[]types.User{user},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
want := []tailcfg.FilterRule{
|
|
{
|
|
SrcIPs: []string{"100.64.0.2/32"},
|
|
DstPorts: []tailcfg.NetPortRange{
|
|
{IP: "100.64.0.1/32", Ports: tailcfg.PortRange{First: 80, Last: 80}},
|
|
{IP: "100.64.0.1/32", Ports: tailcfg.PortRange{First: 443, Last: 443}},
|
|
},
|
|
},
|
|
}
|
|
|
|
if diff := cmp.Diff(want, got); diff != "" {
|
|
t.Errorf("TestValidTagInvalidUser() unexpected result (-want +got):\n%s", diff)
|
|
}
|
|
}
|