mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-23 18:15:26 +00:00
Merge branch 'main' into update-contributors
This commit is contained in:
commit
52db6188df
@ -48,6 +48,7 @@ linters-settings:
|
||||
- ip
|
||||
- ok
|
||||
- c
|
||||
- tt
|
||||
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@ -2,6 +2,24 @@
|
||||
|
||||
**TBD (TBD):**
|
||||
|
||||
**0.14.0 (2022-xx-xx):**
|
||||
|
||||
**UPCOMING BREAKING**:
|
||||
From the **next** version (`0.15.0`), all machines will be able to communicate regardless of
|
||||
if they are in the same namespace. This means that the behaviour currently limited to ACLs
|
||||
will become default. From version `0.15.0`, all limitation of communications must be done
|
||||
with ACLs.
|
||||
|
||||
This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour.
|
||||
|
||||
**BREAKING**:
|
||||
|
||||
- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs
|
||||
- Namespaces are now treated as Users
|
||||
- All machines can communicate with all machines by default
|
||||
- Tags should now work correctly and adding a host to Headscale should now reload the rules.
|
||||
- The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features
|
||||
|
||||
**0.13.0 (2022-02-18):**
|
||||
|
||||
**Features**:
|
||||
|
233
acls.go
233
acls.go
@ -20,7 +20,6 @@ const (
|
||||
errInvalidUserSection = Error("invalid user section")
|
||||
errInvalidGroup = Error("invalid group")
|
||||
errInvalidTag = Error("invalid tag")
|
||||
errInvalidNamespace = Error("invalid namespace")
|
||||
errInvalidPortFormat = Error("invalid port format")
|
||||
)
|
||||
|
||||
@ -69,13 +68,17 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
||||
}
|
||||
|
||||
h.aclPolicy = &policy
|
||||
|
||||
return h.UpdateACLRules()
|
||||
}
|
||||
|
||||
func (h *Headscale) UpdateACLRules() error {
|
||||
rules, err := h.generateACLRules()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.aclRules = rules
|
||||
|
||||
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
||||
h.aclRules = rules
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -83,16 +86,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
||||
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||
rules := []tailcfg.FilterRule{}
|
||||
|
||||
if h.aclPolicy == nil {
|
||||
return nil, errEmptyPolicy
|
||||
}
|
||||
|
||||
machines, err := h.ListAllMachines()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for index, acl := range h.aclPolicy.ACLs {
|
||||
if acl.Action != "accept" {
|
||||
return nil, errInvalidAction
|
||||
}
|
||||
|
||||
filterRule := tailcfg.FilterRule{}
|
||||
|
||||
srcIPs := []string{}
|
||||
for innerIndex, user := range acl.Users {
|
||||
srcs, err := h.generateACLPolicySrcIP(user)
|
||||
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
||||
@ -101,11 +111,10 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||
}
|
||||
srcIPs = append(srcIPs, srcs...)
|
||||
}
|
||||
filterRule.SrcIPs = srcIPs
|
||||
|
||||
destPorts := []tailcfg.NetPortRange{}
|
||||
for innerIndex, ports := range acl.Ports {
|
||||
dests, err := h.generateACLPolicyDestPorts(ports)
|
||||
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
||||
@ -124,11 +133,17 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
|
||||
return h.expandAlias(u)
|
||||
func (h *Headscale) generateACLPolicySrcIP(
|
||||
machines []Machine,
|
||||
aclPolicy ACLPolicy,
|
||||
u string,
|
||||
) ([]string, error) {
|
||||
return expandAlias(machines, aclPolicy, u)
|
||||
}
|
||||
|
||||
func (h *Headscale) generateACLPolicyDestPorts(
|
||||
machines []Machine,
|
||||
aclPolicy ACLPolicy,
|
||||
d string,
|
||||
) ([]tailcfg.NetPortRange, error) {
|
||||
tokens := strings.Split(d, ":")
|
||||
@ -149,11 +164,11 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
||||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||
}
|
||||
|
||||
expanded, err := h.expandAlias(alias)
|
||||
expanded, err := expandAlias(machines, aclPolicy, alias)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ports, err := h.expandPorts(tokens[len(tokens)-1])
|
||||
ports, err := expandPorts(tokens[len(tokens)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -172,21 +187,28 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
||||
return dests, nil
|
||||
}
|
||||
|
||||
func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||
// expandalias has an input of either
|
||||
// - a namespace
|
||||
// - a group
|
||||
// - a tag
|
||||
// and transform these in IPAddresses.
|
||||
func expandAlias(
|
||||
machines []Machine,
|
||||
aclPolicy ACLPolicy,
|
||||
alias string,
|
||||
) ([]string, error) {
|
||||
ips := []string{}
|
||||
if alias == "*" {
|
||||
return []string{"*"}, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "group:") {
|
||||
if _, ok := h.aclPolicy.Groups[alias]; !ok {
|
||||
return nil, errInvalidGroup
|
||||
namespaces, err := expandGroup(aclPolicy, alias)
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
ips := []string{}
|
||||
for _, n := range h.aclPolicy.Groups[alias] {
|
||||
nodes, err := h.ListMachinesInNamespace(n)
|
||||
if err != nil {
|
||||
return nil, errInvalidNamespace
|
||||
}
|
||||
for _, n := range namespaces {
|
||||
nodes := filterMachinesByNamespace(machines, n)
|
||||
for _, node := range nodes {
|
||||
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
||||
}
|
||||
@ -196,35 +218,23 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(alias, "tag:") {
|
||||
if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
|
||||
return nil, errInvalidTag
|
||||
owners, err := expandTagOwners(aclPolicy, alias)
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
|
||||
// This will have HORRIBLE performance.
|
||||
// We need to change the data model to better store tags
|
||||
machines := []Machine{}
|
||||
if err := h.db.Where("registered").Find(&machines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips := []string{}
|
||||
for _, machine := range machines {
|
||||
hostinfo := tailcfg.Hostinfo{}
|
||||
if len(machine.HostInfo) != 0 {
|
||||
hi, err := machine.HostInfo.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, namespace := range owners {
|
||||
machines := filterMachinesByNamespace(machines, namespace)
|
||||
for _, machine := range machines {
|
||||
if len(machine.HostInfo) == 0 {
|
||||
continue
|
||||
}
|
||||
err = json.Unmarshal(hi, &hostinfo)
|
||||
hi, err := machine.GetHostInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return ips, err
|
||||
}
|
||||
|
||||
// FIXME: Check TagOwners allows this
|
||||
for _, t := range hostinfo.RequestTags {
|
||||
if alias[4:] == t {
|
||||
for _, t := range hi.RequestTags {
|
||||
if alias == t {
|
||||
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -233,38 +243,82 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
n, err := h.GetNamespace(alias)
|
||||
if err == nil {
|
||||
nodes, err := h.ListMachinesInNamespace(n.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ips := []string{}
|
||||
for _, n := range nodes {
|
||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||
}
|
||||
|
||||
// if alias is a namespace
|
||||
nodes := filterMachinesByNamespace(machines, alias)
|
||||
nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
|
||||
if err != nil {
|
||||
return ips, err
|
||||
}
|
||||
for _, n := range nodes {
|
||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||
}
|
||||
if len(ips) > 0 {
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
if h, ok := h.aclPolicy.Hosts[alias]; ok {
|
||||
// if alias is an host
|
||||
if h, ok := aclPolicy.Hosts[alias]; ok {
|
||||
return []string{h.String()}, nil
|
||||
}
|
||||
|
||||
// if alias is an IP
|
||||
ip, err := netaddr.ParseIP(alias)
|
||||
if err == nil {
|
||||
return []string{ip.String()}, nil
|
||||
}
|
||||
|
||||
// if alias is an CIDR
|
||||
cidr, err := netaddr.ParseIPPrefix(alias)
|
||||
if err == nil {
|
||||
return []string{cidr.String()}, nil
|
||||
}
|
||||
|
||||
return nil, errInvalidUserSection
|
||||
return ips, errInvalidUserSection
|
||||
}
|
||||
|
||||
func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
||||
// excludeCorrectlyTaggedNodes will remove from the list of input nodes the ones
|
||||
// that are correctly tagged since they should not be listed as being in the namespace
|
||||
// we assume in this function that we only have nodes from 1 namespace.
|
||||
func excludeCorrectlyTaggedNodes(
|
||||
aclPolicy ACLPolicy,
|
||||
nodes []Machine,
|
||||
namespace string,
|
||||
) ([]Machine, error) {
|
||||
out := []Machine{}
|
||||
tags := []string{}
|
||||
for tag, ns := range aclPolicy.TagOwners {
|
||||
if containsString(ns, namespace) {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
// for each machine if tag is in tags list, don't append it.
|
||||
for _, machine := range nodes {
|
||||
if len(machine.HostInfo) == 0 {
|
||||
out = append(out, machine)
|
||||
|
||||
continue
|
||||
}
|
||||
hi, err := machine.GetHostInfo()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
found := false
|
||||
for _, t := range hi.RequestTags {
|
||||
if containsString(tags, t) {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
out = append(out, machine)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
||||
if portsStr == "*" {
|
||||
return &[]tailcfg.PortRange{
|
||||
{First: portRangeBegin, Last: portRangeEnd},
|
||||
@ -306,3 +360,64 @@ func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
||||
|
||||
return &ports, nil
|
||||
}
|
||||
|
||||
func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
|
||||
out := []Machine{}
|
||||
for _, machine := range machines {
|
||||
if machine.Namespace.Name == namespace {
|
||||
out = append(out, machine)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
|
||||
// a group cannot be composed of groups.
|
||||
func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
|
||||
var owners []string
|
||||
ows, ok := aclPolicy.TagOwners[tag]
|
||||
if !ok {
|
||||
return []string{}, fmt.Errorf(
|
||||
"%w. %v isn't owned by a TagOwner. Please add one first. https://tailscale.com/kb/1018/acls/#tag-owners",
|
||||
errInvalidTag,
|
||||
tag,
|
||||
)
|
||||
}
|
||||
for _, owner := range ows {
|
||||
if strings.HasPrefix(owner, "group:") {
|
||||
gs, err := expandGroup(aclPolicy, owner)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
owners = append(owners, gs...)
|
||||
} else {
|
||||
owners = append(owners, owner)
|
||||
}
|
||||
}
|
||||
|
||||
return owners, nil
|
||||
}
|
||||
|
||||
// expandGroup will return the list of namespace inside the group
|
||||
// after some validation.
|
||||
func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
|
||||
groups, ok := aclPolicy.Groups[group]
|
||||
if !ok {
|
||||
return []string{}, fmt.Errorf(
|
||||
"group %v isn't registered. %w",
|
||||
group,
|
||||
errInvalidGroup,
|
||||
)
|
||||
}
|
||||
for _, g := range groups {
|
||||
if strings.HasPrefix(g, "group:") {
|
||||
return []string{}, fmt.Errorf(
|
||||
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
|
||||
errInvalidGroup,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
968
acls_test.go
968
acls_test.go
@ -1,7 +1,14 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"gorm.io/datatypes"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (s *Suite) TestWrongPath(c *check.C) {
|
||||
@ -52,6 +59,245 @@ func (s *Suite) TestBasicRule(c *check.C) {
|
||||
c.Assert(rules, check.NotNil)
|
||||
}
|
||||
|
||||
// TODO(kradalby): Make tests values safe, independent and descriptive.
|
||||
func (s *Suite) TestInvalidAction(c *check.C) {
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{Action: "invalidAction", Users: []string{"*"}, Ports: []string{"*:*"}},
|
||||
},
|
||||
}
|
||||
err := app.UpdateACLRules()
|
||||
c.Assert(errors.Is(err, errInvalidAction), check.Equals, true)
|
||||
}
|
||||
|
||||
func (s *Suite) TestInvalidGroupInGroup(c *check.C) {
|
||||
// this ACL is wrong because the group in users sections doesn't exist
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
Groups: Groups{
|
||||
"group:test": []string{"foo"},
|
||||
"group:error": []string{"foo", "group:test"},
|
||||
},
|
||||
ACLs: []ACL{
|
||||
{Action: "accept", Users: []string{"group:error"}, Ports: []string{"*:*"}},
|
||||
},
|
||||
}
|
||||
err := app.UpdateACLRules()
|
||||
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
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
ACLs: []ACL{
|
||||
{Action: "accept", Users: []string{"tag:foo"}, Ports: []string{"*:*"}},
|
||||
},
|
||||
}
|
||||
err := app.UpdateACLRules()
|
||||
c.Assert(errors.Is(err, errInvalidTag), check.Equals, true)
|
||||
}
|
||||
|
||||
// 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 Users section.
|
||||
func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("user1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = app.GetMachine("user1", "testmachine")
|
||||
c.Assert(err, check.NotNil)
|
||||
hostInfo := []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
|
||||
)
|
||||
machine := Machine{
|
||||
ID: 0,
|
||||
MachineKey: "foo",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostInfo),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||
ACLs: []ACL{
|
||||
{Action: "accept", Users: []string{"tag:test"}, Ports: []string{"*:*"}},
|
||||
},
|
||||
}
|
||||
err = app.UpdateACLRules()
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(app.aclRules, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1")
|
||||
}
|
||||
|
||||
// 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 Ports section.
|
||||
func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("user1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = app.GetMachine("user1", "testmachine")
|
||||
c.Assert(err, check.NotNil)
|
||||
hostInfo := []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:test\"]}",
|
||||
)
|
||||
machine := Machine{
|
||||
ID: 1,
|
||||
MachineKey: "12345",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostInfo),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
Groups: Groups{"group:test": []string{"user1", "user2"}},
|
||||
TagOwners: TagOwners{"tag:test": []string{"user3", "group:test"}},
|
||||
ACLs: []ACL{
|
||||
{Action: "accept", Users: []string{"*"}, Ports: []string{"tag:test:*"}},
|
||||
},
|
||||
}
|
||||
err = app.UpdateACLRules()
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(app.aclRules, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].DstPorts, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1")
|
||||
}
|
||||
|
||||
// need a test with:
|
||||
// tag on a host that isn't owned by a tag owners. So the namespace
|
||||
// of the host should be valid.
|
||||
func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("user1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = app.GetMachine("user1", "testmachine")
|
||||
c.Assert(err, check.NotNil)
|
||||
hostInfo := []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"testmachine\",\"RequestTags\":[\"tag:foo\"]}",
|
||||
)
|
||||
machine := Machine{
|
||||
ID: 1,
|
||||
MachineKey: "12345",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostInfo),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
TagOwners: TagOwners{"tag:test": []string{"user1"}},
|
||||
ACLs: []ACL{
|
||||
{Action: "accept", Users: []string{"user1"}, Ports: []string{"*:*"}},
|
||||
},
|
||||
}
|
||||
err = app.UpdateACLRules()
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(app.aclRules, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.1")
|
||||
}
|
||||
|
||||
// tag on a host is owned by a tag owner, the tag is valid.
|
||||
// an ACL rule is matching the tag to a namespace. It should not be valid since the
|
||||
// host should be tied to the tag now.
|
||||
func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("user1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = app.GetMachine("user1", "webserver")
|
||||
c.Assert(err, check.NotNil)
|
||||
hostInfo := []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"webserver\",\"RequestTags\":[\"tag:webapp\"]}",
|
||||
)
|
||||
machine := Machine{
|
||||
ID: 1,
|
||||
MachineKey: "12345",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "webserver",
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostInfo),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
_, err = app.GetMachine("user1", "user")
|
||||
hostInfo = []byte("{\"OS\":\"debian\",\"Hostname\":\"user\"}")
|
||||
c.Assert(err, check.NotNil)
|
||||
machine = Machine{
|
||||
ID: 2,
|
||||
MachineKey: "56789",
|
||||
NodeKey: "bar2",
|
||||
DiscoKey: "faab",
|
||||
Name: "user",
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
NamespaceID: namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(pak.ID),
|
||||
HostInfo: datatypes.JSON(hostInfo),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
TagOwners: TagOwners{"tag:webapp": []string{"user1"}},
|
||||
ACLs: []ACL{
|
||||
{
|
||||
Action: "accept",
|
||||
Users: []string{"user1"},
|
||||
Ports: []string{"tag:webapp:80,443"},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = app.UpdateACLRules()
|
||||
c.Assert(err, check.IsNil)
|
||||
c.Assert(app.aclRules, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].SrcIPs, check.HasLen, 1)
|
||||
c.Assert(app.aclRules[0].SrcIPs[0], check.Equals, "100.64.0.2")
|
||||
c.Assert(app.aclRules[0].DstPorts, check.HasLen, 2)
|
||||
c.Assert(app.aclRules[0].DstPorts[0].Ports.First, check.Equals, uint16(80))
|
||||
c.Assert(app.aclRules[0].DstPorts[0].Ports.Last, check.Equals, uint16(80))
|
||||
c.Assert(app.aclRules[0].DstPorts[0].IP, check.Equals, "100.64.0.1")
|
||||
c.Assert(app.aclRules[0].DstPorts[1].Ports.First, check.Equals, uint16(443))
|
||||
c.Assert(app.aclRules[0].DstPorts[1].Ports.Last, check.Equals, uint16(443))
|
||||
c.Assert(app.aclRules[0].DstPorts[1].IP, check.Equals, "100.64.0.1")
|
||||
}
|
||||
|
||||
func (s *Suite) TestPortRange(c *check.C) {
|
||||
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
|
||||
c.Assert(err, check.IsNil)
|
||||
@ -94,7 +340,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
|
||||
ips, _ := app.getAvailableIPs()
|
||||
machine := Machine{
|
||||
ID: 0,
|
||||
MachineKey: "foo",
|
||||
MachineKey: "12345",
|
||||
NodeKey: "bar",
|
||||
DiscoKey: "faa",
|
||||
Name: "testmachine",
|
||||
@ -165,3 +411,723 @@ func (s *Suite) TestPortGroup(c *check.C) {
|
||||
c.Assert(len(ips), check.Equals, 1)
|
||||
c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String())
|
||||
}
|
||||
|
||||
func Test_expandGroup(t *testing.T) {
|
||||
type args struct {
|
||||
aclPolicy ACLPolicy
|
||||
group string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple test",
|
||||
args: args{
|
||||
aclPolicy: ACLPolicy{
|
||||
Groups: Groups{
|
||||
"group:test": []string{"user1", "user2", "user3"},
|
||||
"group:foo": []string{"user2", "user3"},
|
||||
},
|
||||
},
|
||||
group: "group:test",
|
||||
},
|
||||
want: []string{"user1", "user2", "user3"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "InexistantGroup",
|
||||
args: args{
|
||||
aclPolicy: ACLPolicy{
|
||||
Groups: Groups{
|
||||
"group:test": []string{"user1", "user2", "user3"},
|
||||
"group:foo": []string{"user2", "user3"},
|
||||
},
|
||||
},
|
||||
group: "group:undefined",
|
||||
},
|
||||
want: []string{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := expandGroup(test.args.aclPolicy, test.args.group)
|
||||
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
|
||||
}
|
||||
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 namespace 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 := expandTagOwners(test.args.aclPolicy, test.args.tag)
|
||||
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
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *[]tailcfg.PortRange
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "wildcard",
|
||||
args: args{portsStr: "*"},
|
||||
want: &[]tailcfg.PortRange{
|
||||
{First: portRangeBegin, Last: portRangeEnd},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "two ports",
|
||||
args: args{portsStr: "80,443"},
|
||||
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"},
|
||||
want: &[]tailcfg.PortRange{
|
||||
{First: 80, Last: 1024},
|
||||
{First: 443, Last: 443},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "out of bounds",
|
||||
args: args{portsStr: "854038"},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong port",
|
||||
args: args{portsStr: "85a38"},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong port in first",
|
||||
args: args{portsStr: "a-80"},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong port in last",
|
||||
args: args{portsStr: "80-85a38"},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong port format",
|
||||
args: args{portsStr: "80-85a38-3"},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := expandPorts(test.args.portsStr)
|
||||
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_listMachinesInNamespace(t *testing.T) {
|
||||
type args struct {
|
||||
machines []Machine
|
||||
namespace string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []Machine
|
||||
}{
|
||||
{
|
||||
name: "1 machine in namespace",
|
||||
args: args{
|
||||
machines: []Machine{
|
||||
{Namespace: Namespace{Name: "joe"}},
|
||||
},
|
||||
namespace: "joe",
|
||||
},
|
||||
want: []Machine{
|
||||
{Namespace: Namespace{Name: "joe"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "3 machines, 2 in namespace",
|
||||
args: args{
|
||||
machines: []Machine{
|
||||
{ID: 1, Namespace: Namespace{Name: "joe"}},
|
||||
{ID: 2, Namespace: Namespace{Name: "marc"}},
|
||||
{ID: 3, Namespace: Namespace{Name: "marc"}},
|
||||
},
|
||||
namespace: "marc",
|
||||
},
|
||||
want: []Machine{
|
||||
{ID: 2, Namespace: Namespace{Name: "marc"}},
|
||||
{ID: 3, Namespace: Namespace{Name: "marc"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "5 machines, 0 in namespace",
|
||||
args: args{
|
||||
machines: []Machine{
|
||||
{ID: 1, Namespace: Namespace{Name: "joe"}},
|
||||
{ID: 2, Namespace: Namespace{Name: "marc"}},
|
||||
{ID: 3, Namespace: Namespace{Name: "marc"}},
|
||||
{ID: 4, Namespace: Namespace{Name: "marc"}},
|
||||
{ID: 5, Namespace: Namespace{Name: "marc"}},
|
||||
},
|
||||
namespace: "mickael",
|
||||
},
|
||||
want: []Machine{},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := filterMachinesByNamespace(test.args.machines, test.args.namespace); !reflect.DeepEqual(
|
||||
got,
|
||||
test.want,
|
||||
) {
|
||||
t.Errorf("listMachinesInNamespace() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// nolint
|
||||
func Test_expandAlias(t *testing.T) {
|
||||
type args struct {
|
||||
machines []Machine
|
||||
aclPolicy ACLPolicy
|
||||
alias string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "wildcard",
|
||||
args: args{
|
||||
alias: "*",
|
||||
machines: []Machine{
|
||||
{IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.78.84.227"),
|
||||
},
|
||||
},
|
||||
},
|
||||
aclPolicy: ACLPolicy{},
|
||||
},
|
||||
want: []string{"*"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "simple group",
|
||||
args: args{
|
||||
alias: "group:accountant",
|
||||
machines: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
aclPolicy: ACLPolicy{
|
||||
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
|
||||
},
|
||||
},
|
||||
want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "wrong group",
|
||||
args: args{
|
||||
alias: "group:hr",
|
||||
machines: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
aclPolicy: ACLPolicy{
|
||||
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
|
||||
},
|
||||
},
|
||||
want: []string{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "simple ipaddress",
|
||||
args: args{
|
||||
alias: "10.0.0.3",
|
||||
machines: []Machine{},
|
||||
aclPolicy: ACLPolicy{},
|
||||
},
|
||||
want: []string{"10.0.0.3"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "private network",
|
||||
args: args{
|
||||
alias: "homeNetwork",
|
||||
machines: []Machine{},
|
||||
aclPolicy: ACLPolicy{
|
||||
Hosts: Hosts{
|
||||
"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []string{"192.168.1.0/24"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "simple host",
|
||||
args: args{
|
||||
alias: "10.0.0.1",
|
||||
machines: []Machine{},
|
||||
aclPolicy: ACLPolicy{},
|
||||
},
|
||||
want: []string{"10.0.0.1"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "simple CIDR",
|
||||
args: args{
|
||||
alias: "10.0.0.0/16",
|
||||
machines: []Machine{},
|
||||
aclPolicy: ACLPolicy{},
|
||||
},
|
||||
want: []string{"10.0.0.0/16"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "simple tag",
|
||||
args: args{
|
||||
alias: "tag:hr-webserver",
|
||||
machines: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:hr-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
aclPolicy: ACLPolicy{
|
||||
TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
|
||||
},
|
||||
},
|
||||
want: []string{"100.64.0.1", "100.64.0.2"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "No tag defined",
|
||||
args: args{
|
||||
alias: "tag:hr-webserver",
|
||||
machines: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
aclPolicy: ACLPolicy{
|
||||
Groups: Groups{"group:accountant": []string{"joe", "marc"}},
|
||||
TagOwners: TagOwners{
|
||||
"tag:accountant-webserver": []string{"group:accountant"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []string{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "list host in namespace without correctly tagged servers",
|
||||
args: args{
|
||||
alias: "joe",
|
||||
machines: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
aclPolicy: ACLPolicy{
|
||||
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
|
||||
},
|
||||
},
|
||||
want: []string{"100.64.0.4"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := expandAlias(
|
||||
test.args.machines,
|
||||
test.args.aclPolicy,
|
||||
test.args.alias,
|
||||
)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)
|
||||
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("expandAlias() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_excludeCorrectlyTaggedNodes(t *testing.T) {
|
||||
type args struct {
|
||||
aclPolicy ACLPolicy
|
||||
nodes []Machine
|
||||
namespace string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []Machine
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "exclude nodes with valid tags",
|
||||
args: args{
|
||||
aclPolicy: ACLPolicy{
|
||||
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
|
||||
},
|
||||
nodes: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"foo\",\"RequestTags\":[\"tag:accountant-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
namespace: "joe",
|
||||
},
|
||||
want: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.4")},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "all nodes have invalid tags, don't exclude them",
|
||||
args: args{
|
||||
aclPolicy: ACLPolicy{
|
||||
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
|
||||
},
|
||||
nodes: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
namespace: "joe",
|
||||
},
|
||||
want: []Machine{
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"hr-web1\",\"RequestTags\":[\"tag:hr-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
HostInfo: []byte(
|
||||
"{\"OS\":\"centos\",\"Hostname\":\"hr-web2\",\"RequestTags\":[\"tag:hr-webserver\"]}",
|
||||
),
|
||||
},
|
||||
{
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.4"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := excludeCorrectlyTaggedNodes(
|
||||
test.args.aclPolicy,
|
||||
test.args.nodes,
|
||||
test.args.namespace,
|
||||
)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf(
|
||||
"excludeCorrectlyTaggedNodes() error = %v, wantErr %v",
|
||||
err,
|
||||
test.wantErr,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("excludeCorrectlyTaggedNodes() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
22
api.go
22
api.go
@ -261,7 +261,16 @@ func (h *Headscale) getMapResponse(
|
||||
|
||||
var respBody []byte
|
||||
if req.Compress == "zstd" {
|
||||
src, _ := json.Marshal(resp)
|
||||
src, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "getMapResponse").
|
||||
Err(err).
|
||||
Msg("Failed to marshal response for the client")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encoder, _ := zstd.NewWriter(nil)
|
||||
srcCompressed := encoder.EncodeAll(src, nil)
|
||||
@ -290,7 +299,16 @@ func (h *Headscale) getMapKeepAliveResponse(
|
||||
var respBody []byte
|
||||
var err error
|
||||
if mapRequest.Compress == "zstd" {
|
||||
src, _ := json.Marshal(mapResponse)
|
||||
src, err := json.Marshal(mapResponse)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "getMapKeepAliveResponse").
|
||||
Err(err).
|
||||
Msg("Failed to marshal keepalive response for the client")
|
||||
|
||||
return nil, err
|
||||
}
|
||||
encoder, _ := zstd.NewWriter(nil)
|
||||
srcCompressed := encoder.EncodeAll(src, nil)
|
||||
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||
|
20
dns.go
20
dns.go
@ -163,7 +163,15 @@ func getMapResponseDNSConfig(
|
||||
dnsConfig = dnsConfigOrig.Clone()
|
||||
dnsConfig.Domains = append(
|
||||
dnsConfig.Domains,
|
||||
fmt.Sprintf("%s.%s", machine.Namespace.Name, baseDomain),
|
||||
fmt.Sprintf(
|
||||
"%s.%s",
|
||||
strings.ReplaceAll(
|
||||
machine.Namespace.Name,
|
||||
"@",
|
||||
".",
|
||||
), // Replace @ with . for valid domain for machine
|
||||
baseDomain,
|
||||
),
|
||||
)
|
||||
|
||||
namespaceSet := set.New(set.ThreadSafe)
|
||||
@ -171,8 +179,14 @@ func getMapResponseDNSConfig(
|
||||
for _, p := range peers {
|
||||
namespaceSet.Add(p.Namespace)
|
||||
}
|
||||
for _, namespace := range namespaceSet.List() {
|
||||
dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain)
|
||||
for _, ns := range namespaceSet.List() {
|
||||
namespace, ok := ns.(Namespace)
|
||||
if !ok {
|
||||
dnsConfig = dnsConfigOrig
|
||||
|
||||
continue
|
||||
}
|
||||
dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
|
||||
dnsConfig.Routes[dnsRoute] = nil
|
||||
}
|
||||
} else {
|
||||
|
@ -39,6 +39,14 @@ use namespaces (which are the equivalent to user/logins in Tailscale.com).
|
||||
|
||||
Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples.
|
||||
|
||||
When using ACL's the Namespace borders are no longer applied. All machines
|
||||
whichever the Namespace have the ability to communicate with other hosts as
|
||||
long as the ACL's permits this exchange.
|
||||
|
||||
The [ACLs](acls.md) document should help understand a fictional case of setting
|
||||
up ACLs in a small company. All concepts presented in this document could be
|
||||
applied outside of business oriented usage.
|
||||
|
||||
### Apple devices
|
||||
|
||||
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
||||
|
141
docs/acls.md
Normal file
141
docs/acls.md
Normal file
@ -0,0 +1,141 @@
|
||||
# ACLs use case example
|
||||
|
||||
Let's build an example use case for a small business (It may be the place where
|
||||
ACL's are the most useful).
|
||||
|
||||
We have a small company with a boss, an admin, two developers and an intern.
|
||||
|
||||
The boss should have access to all servers but not to the users hosts. Admin
|
||||
should also have access to all hosts except that their permissions should be
|
||||
limited to maintaining the hosts (for example purposes). The developers can do
|
||||
anything they want on dev hosts, but only watch on productions hosts. Intern
|
||||
can only interact with the development servers.
|
||||
|
||||
Each user have at least a device connected to the network and we have some
|
||||
servers.
|
||||
|
||||
- database.prod
|
||||
- database.dev
|
||||
- app-server1.prod
|
||||
- app-server1.dev
|
||||
- billing.internal
|
||||
|
||||
## Setup of the network
|
||||
|
||||
Let's create the namespaces. Each user should have his own namespace. The users
|
||||
here are represented as namespaces.
|
||||
|
||||
```bash
|
||||
headscale namespaces create boss
|
||||
headscale namespaces create admin1
|
||||
headscale namespaces create dev1
|
||||
headscale namespaces create dev2
|
||||
headscale namespaces create intern1
|
||||
```
|
||||
|
||||
We don't need to create namespaces for the servers because the servers will be
|
||||
tagged. When registering the servers we will need to add the flag
|
||||
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
||||
registering the server should be allowed to do it. Since anyone can add tags to
|
||||
a server they can register, the check of the tags is done on headscale server
|
||||
and only valid tags are applied. A tag is valid if the namespace that is
|
||||
registering it is allowed to do it.
|
||||
|
||||
Here are the ACL's to implement the same permissions as above:
|
||||
|
||||
```json
|
||||
{
|
||||
// groups are collections of users having a common scope. A user can be in multiple groups
|
||||
// groups cannot be composed of groups
|
||||
"groups": {
|
||||
"group:boss": ["boss"],
|
||||
"group:dev": ["dev1", "dev2"],
|
||||
"group:admin": ["admin1"],
|
||||
"group:intern": ["intern1"]
|
||||
},
|
||||
// tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.
|
||||
// This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag)
|
||||
// and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)
|
||||
"tagOwners": {
|
||||
// the administrators can add servers in production
|
||||
"tag:prod-databases": ["group:admin"],
|
||||
"tag:prod-app-servers": ["group:admin"],
|
||||
|
||||
// the boss can tag any server as internal
|
||||
"tag:internal": ["group:boss"],
|
||||
|
||||
// dev can add servers for dev purposes as well as admins
|
||||
"tag:dev-databases": ["group:admin", "group:dev"],
|
||||
"tag:dev-app-servers": ["group:admin", "group:dev"]
|
||||
|
||||
// interns cannot add servers
|
||||
},
|
||||
"acls": [
|
||||
// boss have access to all servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:boss"],
|
||||
"ports": [
|
||||
"tag:prod-databases:*",
|
||||
"tag:prod-app-servers:*",
|
||||
"tag:internal:*",
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*"
|
||||
]
|
||||
},
|
||||
|
||||
// admin have only access to administrative ports of the servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:admin"],
|
||||
"ports": [
|
||||
"tag:prod-databases:22",
|
||||
"tag:prod-app-servers:22",
|
||||
"tag:internal:22",
|
||||
"tag:dev-databases:22",
|
||||
"tag:dev-app-servers:22"
|
||||
]
|
||||
},
|
||||
|
||||
// developers have access to databases servers and application servers on all ports
|
||||
// they can only view the applications servers in prod and have no access to databases servers in production
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:dev"],
|
||||
"ports": [
|
||||
"tag:dev-databases:*",
|
||||
"tag:dev-app-servers:*",
|
||||
"tag:prod-app-servers:80,443"
|
||||
]
|
||||
},
|
||||
|
||||
// servers should be able to talk to database. Database should not be able to initiate connections to
|
||||
// applications servers
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["tag:dev-app-servers"],
|
||||
"ports": ["tag:dev-databases:5432"]
|
||||
},
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["tag:prod-app-servers"],
|
||||
"ports": ["tag:prod-databases:5432"]
|
||||
},
|
||||
|
||||
// interns have access to dev-app-servers only in reading mode
|
||||
{
|
||||
"action": "accept",
|
||||
"users": ["group:intern"],
|
||||
"ports": ["tag:dev-app-servers:80,443"]
|
||||
},
|
||||
|
||||
// We still have to allow internal namespaces communications since nothing guarantees that each user have
|
||||
// their own namespaces.
|
||||
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
|
||||
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
|
||||
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
|
||||
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
|
||||
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
|
||||
]
|
||||
}
|
||||
```
|
193
machine.go
193
machine.go
@ -119,6 +119,118 @@ func (machine Machine) isExpired() bool {
|
||||
return time.Now().UTC().After(*machine.Expiry)
|
||||
}
|
||||
|
||||
func (h *Headscale) ListAllMachines() ([]Machine, error) {
|
||||
machines := []Machine{}
|
||||
if err := h.db.Preload("AuthKey").
|
||||
Preload("AuthKey.Namespace").
|
||||
Preload("Namespace").
|
||||
Where("registered").
|
||||
Find(&machines).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return machines, nil
|
||||
}
|
||||
|
||||
func containsAddresses(inputs []string, addrs []string) bool {
|
||||
for _, addr := range addrs {
|
||||
if containsString(inputs, addr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// matchSourceAndDestinationWithRule.
|
||||
func matchSourceAndDestinationWithRule(
|
||||
ruleSources []string,
|
||||
ruleDestinations []string,
|
||||
source []string,
|
||||
destination []string,
|
||||
) bool {
|
||||
return containsAddresses(ruleSources, source) &&
|
||||
containsAddresses(ruleDestinations, destination)
|
||||
}
|
||||
|
||||
// getFilteredByACLPeerss should return the list of peers authorized to be accessed from machine.
|
||||
func getFilteredByACLPeers(
|
||||
machines []Machine,
|
||||
rules []tailcfg.FilterRule,
|
||||
machine *Machine,
|
||||
) Machines {
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msg("Finding peers filtered by ACLs")
|
||||
|
||||
peers := make(map[uint64]Machine)
|
||||
// Aclfilter peers here. We are itering through machines in all namespaces and search through the computed aclRules
|
||||
// for match between rule SrcIPs and DstPorts. If the rule is a match we allow the machine to be viewable.
|
||||
|
||||
// FIXME: On official control plane if a rule allow user A to talk to user B but NO rule allows user B to talk to
|
||||
// user A. The behaviour is the following
|
||||
//
|
||||
// On official tailscale control plane:
|
||||
// on first `tailscale status`` on node A we can see node B. The `tailscale status` command on node B doesn't show node A
|
||||
// We can successfully establish a communication from A to B. When it's done, if we run the `tailscale status` command
|
||||
// on node B again we can now see node A. It's not possible to establish a communication from node B to node A.
|
||||
// On this implementation of the feature
|
||||
// on any `tailscale status` command on node A we can see node B. The `tailscale status` command on node B DOES show A.
|
||||
//
|
||||
// I couldn't find a way to not clutter the output of `tailscale status` with all nodes that we could be talking to.
|
||||
// In order to do this we would need to be able to identify that node A want to talk to node B but that Node B doesn't know
|
||||
// how to talk to node A and then add the peering resource.
|
||||
|
||||
for _, peer := range machines {
|
||||
if peer.ID == machine.ID {
|
||||
continue
|
||||
}
|
||||
for _, rule := range rules {
|
||||
var dst []string
|
||||
for _, d := range rule.DstPorts {
|
||||
dst = append(dst, d.IP)
|
||||
}
|
||||
if matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
machine.IPAddresses.ToStringSlice(),
|
||||
peer.IPAddresses.ToStringSlice(),
|
||||
) || // match source and destination
|
||||
matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
machine.IPAddresses.ToStringSlice(),
|
||||
[]string{"*"},
|
||||
) || // match source and all destination
|
||||
matchSourceAndDestinationWithRule(
|
||||
rule.SrcIPs,
|
||||
dst,
|
||||
peer.IPAddresses.ToStringSlice(),
|
||||
machine.IPAddresses.ToStringSlice(),
|
||||
) { // match return path
|
||||
peers[peer.ID] = peer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
authorizedPeers := make([]Machine, 0, len(peers))
|
||||
for _, m := range peers {
|
||||
authorizedPeers = append(authorizedPeers, m)
|
||||
}
|
||||
sort.Slice(
|
||||
authorizedPeers,
|
||||
func(i, j int) bool { return authorizedPeers[i].ID < authorizedPeers[j].ID },
|
||||
)
|
||||
|
||||
log.Trace().
|
||||
Caller().
|
||||
Str("machine", machine.Name).
|
||||
Msgf("Found some machines: %v", machines)
|
||||
|
||||
return authorizedPeers
|
||||
}
|
||||
|
||||
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
|
||||
log.Trace().
|
||||
Caller().
|
||||
@ -206,39 +318,54 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
|
||||
}
|
||||
|
||||
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
||||
direct, err := h.getDirectPeers(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
var peers Machines
|
||||
var err error
|
||||
|
||||
return Machines{}, err
|
||||
// If ACLs rules are defined, filter visible host list with the ACLs
|
||||
// else use the classic namespace scope
|
||||
if h.aclPolicy != nil {
|
||||
var machines []Machine
|
||||
machines, err = h.ListAllMachines()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error retrieving list of machines")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
peers = getFilteredByACLPeers(machines, h.aclRules, machine)
|
||||
} else {
|
||||
direct, err := h.getDirectPeers(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
shared, err := h.getShared(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
sharedTo, err := h.getSharedTo(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
peers = append(direct, shared...)
|
||||
peers = append(peers, sharedTo...)
|
||||
}
|
||||
|
||||
shared, err := h.getShared(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
sharedTo, err := h.getSharedTo(machine)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Cannot fetch peers")
|
||||
|
||||
return Machines{}, err
|
||||
}
|
||||
|
||||
peers := append(direct, shared...)
|
||||
peers = append(peers, sharedTo...)
|
||||
|
||||
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||
|
||||
log.Trace().
|
||||
@ -597,7 +724,11 @@ func (machine Machine) toNode(
|
||||
hostname = fmt.Sprintf(
|
||||
"%s.%s.%s",
|
||||
machine.Name,
|
||||
machine.Namespace.Name,
|
||||
strings.ReplaceAll(
|
||||
machine.Namespace.Name,
|
||||
"@",
|
||||
".",
|
||||
), // Replace @ with . for valid domain for machine
|
||||
baseDomain,
|
||||
)
|
||||
} else {
|
||||
|
262
machine_test.go
262
machine_test.go
@ -1,11 +1,15 @@
|
||||
package headscale
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/check.v1"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (s *Suite) TestGetMachine(c *check.C) {
|
||||
@ -154,6 +158,89 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
|
||||
c.Assert(peersOfMachine0[8].Name, check.Equals, "testmachine10")
|
||||
}
|
||||
|
||||
func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
||||
type base struct {
|
||||
namespace *Namespace
|
||||
key *PreAuthKey
|
||||
}
|
||||
|
||||
stor := make([]base, 0)
|
||||
|
||||
for _, name := range []string{"test", "admin"} {
|
||||
namespace, err := app.CreateNamespace(name)
|
||||
c.Assert(err, check.IsNil)
|
||||
pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
stor = append(stor, base{namespace, pak})
|
||||
}
|
||||
|
||||
_, err := app.GetMachineByID(0)
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
for index := 0; index <= 10; index++ {
|
||||
machine := Machine{
|
||||
ID: uint64(index),
|
||||
MachineKey: "foo" + strconv.Itoa(index),
|
||||
NodeKey: "bar" + strconv.Itoa(index),
|
||||
DiscoKey: "faa" + strconv.Itoa(index),
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP(fmt.Sprintf("100.64.0.%v", strconv.Itoa(index+1))),
|
||||
},
|
||||
Name: "testmachine" + strconv.Itoa(index),
|
||||
NamespaceID: stor[index%2].namespace.ID,
|
||||
Registered: true,
|
||||
RegisterMethod: RegisterMethodAuthKey,
|
||||
AuthKeyID: uint(stor[index%2].key.ID),
|
||||
}
|
||||
app.db.Save(&machine)
|
||||
}
|
||||
|
||||
app.aclPolicy = &ACLPolicy{
|
||||
Groups: map[string][]string{
|
||||
"group:test": {"admin"},
|
||||
},
|
||||
Hosts: map[string]netaddr.IPPrefix{},
|
||||
TagOwners: map[string][]string{},
|
||||
ACLs: []ACL{
|
||||
{Action: "accept", Users: []string{"admin"}, Ports: []string{"*:*"}},
|
||||
{Action: "accept", Users: []string{"test"}, Ports: []string{"test:*"}},
|
||||
},
|
||||
Tests: []ACLTest{},
|
||||
}
|
||||
|
||||
err = app.UpdateACLRules()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
adminMachine, err := app.GetMachineByID(1)
|
||||
c.Logf("Machine(%v), namespace: %v", adminMachine.Name, adminMachine.Namespace)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
testMachine, err := app.GetMachineByID(2)
|
||||
c.Logf("Machine(%v), namespace: %v", testMachine.Name, testMachine.Namespace)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = testMachine.GetHostInfo()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
machines, err := app.ListAllMachines()
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine)
|
||||
peersOfAdminMachine := getFilteredByACLPeers(machines, app.aclRules, adminMachine)
|
||||
|
||||
c.Log(peersOfTestMachine)
|
||||
c.Assert(len(peersOfTestMachine), check.Equals, 4)
|
||||
c.Assert(peersOfTestMachine[0].Name, check.Equals, "testmachine4")
|
||||
c.Assert(peersOfTestMachine[1].Name, check.Equals, "testmachine6")
|
||||
c.Assert(peersOfTestMachine[3].Name, check.Equals, "testmachine10")
|
||||
|
||||
c.Log(peersOfAdminMachine)
|
||||
c.Assert(len(peersOfAdminMachine), check.Equals, 9)
|
||||
c.Assert(peersOfAdminMachine[0].Name, check.Equals, "testmachine2")
|
||||
c.Assert(peersOfAdminMachine[2].Name, check.Equals, "testmachine4")
|
||||
c.Assert(peersOfAdminMachine[5].Name, check.Equals, "testmachine7")
|
||||
}
|
||||
|
||||
func (s *Suite) TestExpireMachine(c *check.C) {
|
||||
namespace, err := app.CreateNamespace("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
@ -208,3 +295,178 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
|
||||
c.Assert(deserialized[i], check.Equals, input[i])
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getFilteredByACLPeers(t *testing.T) {
|
||||
type args struct {
|
||||
machines []Machine
|
||||
rules []tailcfg.FilterRule
|
||||
machine *Machine
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Machines
|
||||
}{
|
||||
{
|
||||
name: "all hosts can talk to each other",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{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: &Machine{ // current machine
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "One host can talk to another, but not all hosts",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{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: &Machine{ // current machine
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "host cannot directly talk to destination, but return path is authorized",
|
||||
args: args{
|
||||
machines: []Machine{ // list of all machines in the database
|
||||
{
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.1"),
|
||||
},
|
||||
Namespace: Namespace{Name: "joe"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.2"),
|
||||
},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{
|
||||
netaddr.MustParseIP("100.64.0.3"),
|
||||
},
|
||||
Namespace: Namespace{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: &Machine{ // current machine
|
||||
ID: 1,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
|
||||
Namespace: Namespace{Name: "marc"},
|
||||
},
|
||||
},
|
||||
want: Machines{
|
||||
{
|
||||
ID: 3,
|
||||
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
|
||||
Namespace: Namespace{Name: "mickael"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := getFilteredByACLPeers(
|
||||
tt.args.machines,
|
||||
tt.args.rules,
|
||||
tt.args.machine,
|
||||
)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("getFilteredByACLPeers() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
28
poll.go
28
poll.go
@ -85,12 +85,26 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
||||
Str("machine", machine.Name).
|
||||
Msg("Found machine in database")
|
||||
|
||||
hostinfo, _ := json.Marshal(req.Hostinfo)
|
||||
hostinfo, err := json.Marshal(req.Hostinfo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
machine.Name = req.Hostinfo.Hostname
|
||||
machine.HostInfo = datatypes.JSON(hostinfo)
|
||||
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
|
||||
now := time.Now().UTC()
|
||||
|
||||
// update ACLRules with peer informations (to update server tags if necessary)
|
||||
if h.aclPolicy != nil {
|
||||
err = h.UpdateACLRules()
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "handleAuthKey").
|
||||
Str("machine", machine.Name).
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
// From Tailscale client:
|
||||
//
|
||||
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
||||
@ -100,7 +114,17 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) {
|
||||
// The intended use is for clients to discover the DERP map at start-up
|
||||
// before their first real endpoint update.
|
||||
if !req.ReadOnly {
|
||||
endpoints, _ := json.Marshal(req.Endpoints)
|
||||
endpoints, err := json.Marshal(req.Endpoints)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "PollNetMapHandler").
|
||||
Err(err).
|
||||
Msg("Failed to mashal requested endpoints for the client")
|
||||
ctx.String(http.StatusInternalServerError, ":(")
|
||||
|
||||
return
|
||||
}
|
||||
machine.Endpoints = datatypes.JSON(endpoints)
|
||||
machine.LastSeen = &now
|
||||
}
|
||||
|
10
utils.go
10
utils.go
@ -212,6 +212,16 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
|
||||
return ips, nil
|
||||
}
|
||||
|
||||
func containsString(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
|
||||
for _, v := range ips {
|
||||
if v == ip {
|
||||
|
Loading…
Reference in New Issue
Block a user