mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Merge pull request #320 from restanrm/feat-improve-acls-usage
Improvements on the ACLs and bug fixing
This commit is contained in:
		
						commit
						69cdfbb56f
					
				@ -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**:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										219
									
								
								acls.go
									
									
									
									
									
								
							
							
						
						
									
										219
									
								
								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
 | 
			
		||||
		}
 | 
			
		||||
		ips := []string{}
 | 
			
		||||
		for _, n := range h.aclPolicy.Groups[alias] {
 | 
			
		||||
			nodes, err := h.ListMachinesInNamespace(n)
 | 
			
		||||
		namespaces, err := expandGroup(aclPolicy, alias)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
				return nil, errInvalidNamespace
 | 
			
		||||
			return ips, err
 | 
			
		||||
		}
 | 
			
		||||
		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 _, namespace := range owners {
 | 
			
		||||
			machines := filterMachinesByNamespace(machines, namespace)
 | 
			
		||||
			for _, machine := range machines {
 | 
			
		||||
			hostinfo := tailcfg.Hostinfo{}
 | 
			
		||||
			if len(machine.HostInfo) != 0 {
 | 
			
		||||
				hi, err := machine.HostInfo.MarshalJSON()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				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 alias is a namespace
 | 
			
		||||
	nodes := filterMachinesByNamespace(machines, alias)
 | 
			
		||||
	nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		return ips, err
 | 
			
		||||
	}
 | 
			
		||||
		ips := []string{}
 | 
			
		||||
	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:*"] }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										135
									
								
								machine.go
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								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,6 +318,21 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
 | 
			
		||||
	var peers Machines
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	// 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().
 | 
			
		||||
@ -235,9 +362,9 @@ func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
 | 
			
		||||
 | 
			
		||||
			return Machines{}, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	peers := append(direct, shared...)
 | 
			
		||||
		peers = append(direct, shared...)
 | 
			
		||||
		peers = append(peers, sharedTo...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
 | 
			
		||||
 | 
			
		||||
@ -597,7 +724,11 @@ func (machine Machine) toNode(
 | 
			
		||||
		hostname = fmt.Sprintf(
 | 
			
		||||
			"%s.%s.%s",
 | 
			
		||||
			machine.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