headscale/hscontrol/policy/acls_test.go
Kristoffer Dalby 9c425a1c08 Finish SSH
This commit allows SSH rules to be assigned to each relevant not and
by doing that allow SSH to be rejected, completing the initial SSH
support.

This commit enables SSH by default and removes the experimental flag.

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2023-06-21 10:31:48 +02:00

2598 lines
60 KiB
Go

package policy
import (
"errors"
"net/netip"
"reflect"
"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/stretchr/testify/assert"
"go4.org/netipx"
"gopkg.in/check.v1"
"tailscale.com/tailcfg"
)
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 (s *Suite) TestBrokenHuJson(c *check.C) {
acl := []byte(`
{
`)
_, err := LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.NotNil)
}
func (s *Suite) TestInvalidPolicyHuson(c *check.C) {
acl := []byte(`
{
"valid_json": true,
"but_a_policy_though": false
}
`)
_, err := LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.NotNil)
c.Assert(err, check.Equals, ErrEmptyPolicy)
}
func (s *Suite) TestParseHosts(c *check.C) {
var hosts Hosts
err := hosts.UnmarshalJSON(
[]byte(
`{"example-host-1": "100.100.100.100","example-host-2": "100.100.101.100/24"}`,
),
)
c.Assert(hosts, check.NotNil)
c.Assert(err, check.IsNil)
}
func (s *Suite) TestParseInvalidCIDR(c *check.C) {
var hosts Hosts
err := hosts.UnmarshalJSON([]byte(`{"example-host-1": "100.100.100.100/42"}`))
c.Assert(hosts, check.IsNil)
c.Assert(err, check.NotNil)
}
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": [
"exapmle-host-2:100"
],
},
{
"src": "user2@example.com",
"accept": [
"100.60.3.4:22"
],
},
],
}
`)
pol, err := LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(pol.ACLs, check.HasLen, 6)
c.Assert(err, check.IsNil)
rules, err := pol.generateFilterRules(types.Machines{}, false)
c.Assert(err, check.NotNil)
c.Assert(rules, check.IsNil)
}
func (s *Suite) TestBasicRule(c *check.C) {
acl := []byte(`
{
"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:*",
],
},
],
}
`)
pol, err := LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil)
rules, err := pol.generateFilterRules(types.Machines{}, false)
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
}
// 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 := GenerateFilterRules(pol, &types.Machine{}, types.Machines{}, false)
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 := GenerateFilterRules(pol, &types.Machine{}, types.Machines{}, false)
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 := GenerateFilterRules(pol, &types.Machine{}, types.Machines{}, false)
c.Assert(errors.Is(err, ErrInvalidTag), check.Equals, true)
}
func (s *Suite) TestPortRange(c *check.C) {
acl := []byte(`
{
"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",
],
},
],
}
`)
pol, err := LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil)
c.Assert(pol, check.NotNil)
rules, err := pol.generateFilterRules(types.Machines{}, false)
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 1)
c.Assert(rules[0].DstPorts, check.HasLen, 1)
c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(5400))
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500))
}
func (s *Suite) TestProtocolParsing(c *check.C) {
acl := []byte(`
{
"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:*",
],
},
],
}
`)
pol, err := LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil)
c.Assert(pol, check.NotNil)
rules, err := pol.generateFilterRules(types.Machines{}, false)
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 3)
c.Assert(rules[0].IPProto[0], check.Equals, protocolTCP)
c.Assert(rules[1].IPProto[0], check.Equals, protocolUDP)
c.Assert(rules[2].IPProto[1], check.Equals, protocolIPv6ICMP)
}
func (s *Suite) TestPortWildcard(c *check.C) {
acl := []byte(`
{
"hosts": {
"host-1": "100.100.100.100",
"subnet-1": "100.100.101.100/24",
},
"acls": [
{
"Action": "accept",
"src": [
"*",
],
"dst": [
"host-1:*",
],
},
],
}
`)
pol, err := LoadACLPolicyFromBytes(acl, "hujson")
c.Assert(err, check.IsNil)
c.Assert(pol, check.NotNil)
rules, err := pol.generateFilterRules(types.Machines{}, false)
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 1)
c.Assert(rules[0].DstPorts, check.HasLen, 1)
c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert(rules[0].SrcIPs, check.HasLen, 2)
c.Assert(rules[0].SrcIPs[0], check.Equals, "0.0.0.0/0")
}
func (s *Suite) TestPortWildcardYAML(c *check.C) {
acl := []byte(`---
hosts:
host-1: 100.100.100.100/32
subnet-1: 100.100.101.100/24
acls:
- action: accept
src:
- "*"
dst:
- host-1:*`)
pol, err := LoadACLPolicyFromBytes(acl, "yaml")
c.Assert(err, check.IsNil)
c.Assert(pol, check.NotNil)
rules, err := pol.generateFilterRules(types.Machines{}, false)
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 1)
c.Assert(rules[0].DstPorts, check.HasLen, 1)
c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert(rules[0].SrcIPs, check.HasLen, 2)
c.Assert(rules[0].SrcIPs[0], check.Equals, "0.0.0.0/0")
}
func (s *Suite) TestBasicIpv6YAML(c *check.C) {
acl := []byte(`
---
hosts:
host-1: 100.100.100.100/32
subnet-1: 100.100.101.100/24
acls:
- action: accept
src:
- "*"
dst:
- 0.0.0.0/0:*
- ::/0:*
- fd7a:115c:a1e0::2:22
`)
pol, err := LoadACLPolicyFromBytes(acl, "yaml")
c.Assert(err, check.IsNil)
c.Assert(pol, check.NotNil)
rules, err := pol.generateFilterRules(types.Machines{}, false)
c.Assert(err, check.IsNil)
c.Assert(rules, check.NotNil)
c.Assert(rules, check.HasLen, 1)
c.Assert(rules[0].DstPorts, check.HasLen, 3)
c.Assert(rules[0].DstPorts[0].IP, check.Equals, "0.0.0.0/0")
c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0))
c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535))
c.Assert(rules[0].DstPorts[1].IP, check.Equals, "::/0")
c.Assert(rules[0].DstPorts[1].Ports.First, check.Equals, uint16(0))
c.Assert(rules[0].DstPorts[1].Ports.Last, check.Equals, uint16(65535))
c.Assert(rules[0].DstPorts[2].IP, check.Equals, "fd7a:115c:a1e0::2/128")
c.Assert(rules[0].DstPorts[2].Ports.First, check.Equals, uint16(22))
c.Assert(rules[0].DstPorts[2].Ports.Last, check.Equals, uint16(22))
c.Assert(rules[0].SrcIPs, check.HasLen, 2)
c.Assert(rules[0].SrcIPs[0], check.Equals, "0.0.0.0/0")
}
func Test_expandGroup(t *testing.T) {
type field struct {
pol ACLPolicy
}
type args struct {
group string
stripEmailDomain 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",
stripEmailDomain: true,
},
want: []string{"user1", "user2", "user3"},
wantErr: false,
},
{
name: "InexistantGroup",
field: field{
pol: ACLPolicy{
Groups: Groups{
"group:test": []string{"user1", "user2", "user3"},
"group:foo": []string{"user2", "user3"},
},
},
},
args: args{
group: "group:undefined",
stripEmailDomain: true,
},
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",
stripEmailDomain: true,
},
want: []string{"joe.bar", "john.doe"},
wantErr: false,
},
{
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",
stripEmailDomain: false,
},
want: []string{"joe.bar.gmail.com", "john.doe.yahoo.fr"},
wantErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.field.pol.getUsersInGroup(
test.args.group,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("expandGroup() = %v, want %v", got, test.want)
}
})
}
}
func Test_expandTagOwners(t *testing.T) {
type args struct {
aclPolicy *ACLPolicy
tag string
stripEmailDomain bool
}
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",
stripEmailDomain: true,
},
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",
stripEmailDomain: true,
},
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",
stripEmailDomain: true,
},
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",
stripEmailDomain: true,
},
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",
stripEmailDomain: true,
},
want: []string{},
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := getTagOwners(
test.args.aclPolicy,
test.args.tag,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr {
t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
return
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("expandTagOwners() = %v, want %v", got, test.want)
}
})
}
}
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 !reflect.DeepEqual(got, test.want) {
t.Errorf("expandPorts() = %v, want %v", got, test.want)
}
})
}
}
func Test_listMachinesInUser(t *testing.T) {
type args struct {
machines types.Machines
user string
}
tests := []struct {
name string
args args
want types.Machines
}{
{
name: "1 machine in user",
args: args{
machines: types.Machines{
{User: types.User{Name: "joe"}},
},
user: "joe",
},
want: types.Machines{
{User: types.User{Name: "joe"}},
},
},
{
name: "3 machines, 2 in user",
args: args{
machines: types.Machines{
{ID: 1, User: types.User{Name: "joe"}},
{ID: 2, User: types.User{Name: "marc"}},
{ID: 3, User: types.User{Name: "marc"}},
},
user: "marc",
},
want: types.Machines{
{ID: 2, User: types.User{Name: "marc"}},
{ID: 3, User: types.User{Name: "marc"}},
},
},
{
name: "5 machines, 0 in user",
args: args{
machines: types.Machines{
{ID: 1, User: types.User{Name: "joe"}},
{ID: 2, User: types.User{Name: "marc"}},
{ID: 3, User: types.User{Name: "marc"}},
{ID: 4, User: types.User{Name: "marc"}},
{ID: 5, User: types.User{Name: "marc"}},
},
user: "mickael",
},
want: types.Machines{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := filterMachinesByUser(test.args.machines, test.args.user); !reflect.DeepEqual(
got,
test.want,
) {
t.Errorf("listMachinesInUser() = %v, want %v", got, test.want)
}
})
}
}
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
}
type field struct {
pol ACLPolicy
}
type args struct {
machines types.Machines
aclPolicy ACLPolicy
alias string
stripEmailDomain bool
}
tests := []struct {
name string
field field
args args
want *netipx.IPSet
wantErr bool
}{
{
name: "wildcard",
field: field{
pol: ACLPolicy{},
},
args: args{
alias: "*",
machines: types.Machines{
{IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.1")}},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.78.84.227"),
},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "marc"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "marc"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
want: set([]string{}, []string{}),
wantErr: true,
},
{
name: "simple ipaddress",
field: field{
pol: ACLPolicy{},
},
args: args{
alias: "10.0.0.3",
machines: types.Machines{},
stripEmailDomain: true,
},
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",
machines: types.Machines{},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("10.0.0.1"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("10.0.0.1"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("10.0.0.1"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{},
stripEmailDomain: true,
},
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",
machines: types.Machines{},
stripEmailDomain: true,
},
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",
machines: types.Machines{},
aclPolicy: ACLPolicy{},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "marc"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "joe"},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "marc"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
want: set([]string{}, []string{}),
wantErr: true,
},
{
name: "Forced tag defined",
field: field{
pol: ACLPolicy{},
},
args: args{
alias: "tag:hr-webserver",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "marc"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
ForcedTags: []string{"tag:hr-webserver"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "marc"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
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",
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "marc"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "joe"},
},
},
stripEmailDomain: true,
},
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.machines,
test.args.alias,
test.args.stripEmailDomain,
)
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.Machines
user string
stripEmailDomain bool
}
tests := []struct {
name string
args args
want types.Machines
wantErr bool
}{
{
name: "exclude nodes with valid tags",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
nodes: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "joe"},
},
},
user: "joe",
stripEmailDomain: true,
},
want: types.Machines{
{
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.4")},
User: types.User{Name: "joe"},
},
},
},
{
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.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "joe"},
},
},
user: "joe",
stripEmailDomain: true,
},
want: types.Machines{
{
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.4")},
User: types.User{Name: "joe"},
},
},
},
{
name: "exclude nodes with valid tags and with forced tags",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
nodes: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "foo",
RequestTags: []string{"tag:accountant-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
ForcedTags: []string{"tag:accountant-webserver"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "joe"},
},
},
user: "joe",
stripEmailDomain: true,
},
want: types.Machines{
{
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.4")},
User: types.User{Name: "joe"},
},
},
},
{
name: "all nodes have invalid tags, don't exclude them",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
},
nodes: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "joe"},
},
},
user: "joe",
stripEmailDomain: true,
},
want: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "hr-web1",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "joe"},
HostInfo: types.HostInfo{
OS: "centos",
Hostname: "hr-web2",
RequestTags: []string{"tag:hr-webserver"},
},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
},
User: types.User{Name: "joe"},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := excludeCorrectlyTaggedNodes(
test.args.aclPolicy,
test.args.nodes,
test.args.user,
test.args.stripEmailDomain,
)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
}
})
}
}
func TestACLPolicy_generateFilterRules(t *testing.T) {
type field struct {
pol ACLPolicy
}
type args struct {
machines types.Machines
stripEmailDomain bool
}
tests := []struct {
name string
field field
args args
want []tailcfg.FilterRule
wantErr bool
}{
{
name: "no-policy",
field: field{},
args: args{},
want: []tailcfg.FilterRule{},
wantErr: false,
},
{
name: "allow-all",
field: field{
pol: ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Sources: []string{"*"},
Destinations: []string{"*:*"},
},
},
},
},
args: args{
machines: types.Machines{},
stripEmailDomain: true,
},
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",
field: field{
pol: ACLPolicy{
ACLs: []ACL{
{
Action: "accept",
Sources: []string{"100.64.0.1"},
Destinations: []string{"100.64.0.2:*"},
},
},
},
},
args: args{
machines: types.Machines{
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2221"),
},
User: types.User{Name: "mickael"},
},
{
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:2222:6273:2222"),
},
User: types.User{Name: "mickael"},
},
},
stripEmailDomain: true,
},
want: []tailcfg.FilterRule{
{
SrcIPs: []string{
"100.64.0.1/32",
"fd7a:115c:a1e0:ab12:4843:2222:6273:2221/128",
},
DstPorts: []tailcfg.NetPortRange{
{
IP: "100.64.0.2/32",
Ports: tailcfg.PortRange{
First: 0,
Last: 65535,
},
},
{
IP: "fd7a:115c:a1e0:ab12:4843:2222:6273:2222/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.generateFilterRules(
tt.args.machines,
tt.args.stripEmailDomain,
)
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)
}
})
}
}
func Test_getTags(t *testing.T) {
type args struct {
aclPolicy *ACLPolicy
machine types.Machine
stripEmailDomain bool
}
tests := []struct {
name string
args args
wantInvalid []string
wantValid []string
}{
{
name: "valid tag one machine",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{
"tag:valid": []string{"joe"},
},
},
machine: types.Machine{
User: types.User{
Name: "joe",
},
HostInfo: types.HostInfo{
RequestTags: []string{"tag:valid"},
},
},
stripEmailDomain: false,
},
wantValid: []string{"tag:valid"},
wantInvalid: nil,
},
{
name: "invalid tag and valid tag one machine",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{
"tag:valid": []string{"joe"},
},
},
machine: types.Machine{
User: types.User{
Name: "joe",
},
HostInfo: types.HostInfo{
RequestTags: []string{"tag:valid", "tag:invalid"},
},
},
stripEmailDomain: false,
},
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"},
},
},
machine: types.Machine{
User: types.User{
Name: "joe",
},
HostInfo: types.HostInfo{
RequestTags: []string{
"tag:invalid",
"tag:valid",
"tag:invalid",
},
},
},
stripEmailDomain: false,
},
wantValid: []string{"tag:valid"},
wantInvalid: []string{"tag:invalid"},
},
{
name: "only invalid tags",
args: args{
aclPolicy: &ACLPolicy{
TagOwners: TagOwners{
"tag:valid": []string{"joe"},
},
},
machine: types.Machine{
User: types.User{
Name: "joe",
},
HostInfo: types.HostInfo{
RequestTags: []string{"tag:invalid", "very-invalid"},
},
},
stripEmailDomain: false,
},
wantValid: nil,
wantInvalid: []string{"tag:invalid", "very-invalid"},
},
{
name: "empty ACLPolicy should return empty tags and should not panic",
args: args{
aclPolicy: &ACLPolicy{},
machine: types.Machine{
User: types.User{
Name: "joe",
},
HostInfo: types.HostInfo{
RequestTags: []string{"tag:invalid", "very-invalid"},
},
},
stripEmailDomain: false,
},
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.GetTagsOfMachine(
test.args.machine,
test.args.stripEmailDomain,
)
for _, valid := range gotValid {
if !util.StringOrPrefixListContains(test.wantValid, valid) {
t.Errorf(
"valids: getTags() = %v, want %v",
gotValid,
test.wantValid,
)
break
}
}
for _, invalid := range gotInvalid {
if !util.StringOrPrefixListContains(test.wantInvalid, invalid) {
t.Errorf(
"invalids: getTags() = %v, want %v",
gotInvalid,
test.wantInvalid,
)
break
}
}
})
}
}
func Test_getFilteredByACLPeers(t *testing.T) {
type args struct {
machines types.Machines
rules []tailcfg.FilterRule
machine *types.Machine
}
tests := []struct {
name string
args args
want types.Machines
}{
{
name: "all hosts can talk to each other",
args: args{
machines: types.Machines{ // list of all machines in the database
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("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: "*"},
},
},
},
machine: &types.Machine{ // current machine
ID: 1,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.1")},
User: types.User{Name: "joe"},
},
},
want: types.Machines{
{
ID: 2,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.2")},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.3")},
User: types.User{Name: "mickael"},
},
},
},
{
name: "One host can talk to another, but not all hosts",
args: args{
machines: types.Machines{ // list of all machines in the database
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("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"},
},
},
},
machine: &types.Machine{ // current machine
ID: 1,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.1")},
User: types.User{Name: "joe"},
},
},
want: types.Machines{
{
ID: 2,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.2")},
User: types.User{Name: "marc"},
},
},
},
{
name: "host cannot directly talk to destination, but return path is authorized",
args: args{
machines: types.Machines{ // list of all machines in the database
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("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"},
},
},
},
machine: &types.Machine{ // current machine
ID: 2,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.2")},
User: types.User{Name: "marc"},
},
},
want: types.Machines{
{
ID: 3,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.3")},
User: types.User{Name: "mickael"},
},
},
},
{
name: "rules allows all hosts to reach one destination",
args: args{
machines: types.Machines{ // list of all machines in the database
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("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"},
},
},
},
machine: &types.Machine{ // current machine
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
},
want: types.Machines{
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("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{
machines: types.Machines{ // list of all machines in the database
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("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"},
},
},
},
machine: &types.Machine{ // current machine
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
},
want: types.Machines{
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "mickael"},
},
},
},
{
name: "rule allows all hosts to reach all destinations",
args: args{
machines: types.Machines{ // list of all machines in the database
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "*"},
},
},
},
machine: &types.Machine{ // current machine
ID: 2,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.2")},
User: types.User{Name: "marc"},
},
},
want: types.Machines{
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.3")},
User: types.User{Name: "mickael"},
},
},
},
{
name: "without rule all communications are forbidden",
args: args{
machines: types.Machines{ // list of all machines in the database
{
ID: 1,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
},
User: types.User{Name: "joe"},
},
{
ID: 2,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
},
User: types.User{Name: "marc"},
},
{
ID: 3,
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
},
User: types.User{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
},
machine: &types.Machine{ // current machine
ID: 2,
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.2")},
User: types.User{Name: "marc"},
},
},
want: types.Machines{},
},
{
// Investigating 699
// Found some machines: [ts-head-8w6paa ts-unstable-lys2ib ts-head-upcrmb ts-unstable-rlwpvr] machine=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{
machines: types.Machines{ //
{
ID: 1,
Hostname: "ts-head-upcrmb",
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
netip.MustParseAddr("fd7a:115c:a1e0::3"),
},
User: types.User{Name: "user1"},
},
{
ID: 2,
Hostname: "ts-unstable-rlwpvr",
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
netip.MustParseAddr("fd7a:115c:a1e0::4"),
},
User: types.User{Name: "user1"},
},
{
ID: 3,
Hostname: "ts-head-8w6paa",
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
netip.MustParseAddr("fd7a:115c:a1e0::1"),
},
User: types.User{Name: "user2"},
},
{
ID: 4,
Hostname: "ts-unstable-lys2ib",
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.2"),
netip.MustParseAddr("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",
},
},
},
machine: &types.Machine{ // current machine
ID: 3,
Hostname: "ts-head-8w6paa",
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.1"),
netip.MustParseAddr("fd7a:115c:a1e0::1"),
},
User: types.User{Name: "user2"},
},
},
want: types.Machines{
{
ID: 1,
Hostname: "ts-head-upcrmb",
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.3"),
netip.MustParseAddr("fd7a:115c:a1e0::3"),
},
User: types.User{Name: "user1"},
},
{
ID: 2,
Hostname: "ts-unstable-rlwpvr",
IPAddresses: types.MachineAddresses{
netip.MustParseAddr("100.64.0.4"),
netip.MustParseAddr("fd7a:115c:a1e0::4"),
},
User: types.User{Name: "user1"},
},
},
},
{
name: "failing-edge-case-during-p3-refactor",
args: args{
machines: []types.Machine{
{
ID: 1,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")},
Hostname: "peer1",
User: types.User{Name: "mini"},
},
{
ID: 2,
IPAddresses: []netip.Addr{netip.MustParseAddr("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},
},
},
},
machine: &types.Machine{
ID: 0,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")},
Hostname: "mini",
User: types.User{Name: "mini"},
},
},
want: []types.Machine{
{
ID: 2,
IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.3")},
Hostname: "peer2",
User: types.User{Name: "peer2"},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FilterMachinesByACL(
tt.args.machine,
tt.args.machines,
tt.args.rules,
)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("filterMachinesByACL() = %v, want %v", got, tt.want)
}
})
}
}
func TestSSHRules(t *testing.T) {
tests := []struct {
name string
machine types.Machine
peers types.Machines
pol ACLPolicy
want []*tailcfg.SSHRule
}{
{
name: "peers-can-connect",
machine: types.Machine{
Hostname: "testmachine",
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.99.42")},
UserID: 0,
User: types.User{
Name: "user1",
},
},
peers: types.Machines{
types.Machine{
Hostname: "testmachine2",
IPAddresses: types.MachineAddresses{netip.MustParseAddr("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.SSHRule{
{
Principals: []*tailcfg.SSHPrincipal{
{
UserLogin: "user1",
},
},
SSHUsers: map[string]string{
"autogroup:nonroot": "=",
},
Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true},
},
{
SSHUsers: map[string]string{
"autogroup:nonroot": "=",
},
Principals: []*tailcfg.SSHPrincipal{
{
Any: true,
},
},
Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true},
},
{
Principals: []*tailcfg.SSHPrincipal{
{
UserLogin: "user1",
},
},
SSHUsers: map[string]string{
"autogroup:nonroot": "=",
},
Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true},
},
{
SSHUsers: map[string]string{
"autogroup:nonroot": "=",
},
Principals: []*tailcfg.SSHPrincipal{
{
Any: true,
},
},
Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true},
},
},
},
{
name: "peers-cannot-connect",
machine: types.Machine{
Hostname: "testmachine",
IPAddresses: types.MachineAddresses{netip.MustParseAddr("100.64.0.1")},
UserID: 0,
User: types.User{
Name: "user1",
},
},
peers: types.Machines{
types.Machine{
Hostname: "testmachine2",
IPAddresses: types.MachineAddresses{netip.MustParseAddr("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.SSHRule{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.pol.generateSSHRules(&tt.machine, tt.peers, false)
assert.NoError(t, err)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("TestSSHRules() unexpected result (-want +got):\n%s", diff)
}
})
}
}