mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-26 08:53:05 +00:00
Merge branch 'main' into configurable-mtls
This commit is contained in:
commit
f562ad579a
12 changed files with 1808 additions and 98 deletions
|
@ -48,6 +48,7 @@ linters-settings:
|
||||||
- ip
|
- ip
|
||||||
- ok
|
- ok
|
||||||
- c
|
- c
|
||||||
|
- tt
|
||||||
|
|
||||||
gocritic:
|
gocritic:
|
||||||
disabled-checks:
|
disabled-checks:
|
||||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -2,6 +2,26 @@
|
||||||
|
|
||||||
**TBD (TBD):**
|
**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
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
|
||||||
- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls)
|
- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls)
|
||||||
|
|
||||||
**0.13.0 (2022-02-18):**
|
**0.13.0 (2022-02-18):**
|
||||||
|
|
233
acls.go
233
acls.go
|
@ -20,7 +20,6 @@ const (
|
||||||
errInvalidUserSection = Error("invalid user section")
|
errInvalidUserSection = Error("invalid user section")
|
||||||
errInvalidGroup = Error("invalid group")
|
errInvalidGroup = Error("invalid group")
|
||||||
errInvalidTag = Error("invalid tag")
|
errInvalidTag = Error("invalid tag")
|
||||||
errInvalidNamespace = Error("invalid namespace")
|
|
||||||
errInvalidPortFormat = Error("invalid port format")
|
errInvalidPortFormat = Error("invalid port format")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,13 +68,17 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.aclPolicy = &policy
|
h.aclPolicy = &policy
|
||||||
|
|
||||||
|
return h.UpdateACLRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Headscale) UpdateACLRules() error {
|
||||||
rules, err := h.generateACLRules()
|
rules, err := h.generateACLRules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h.aclRules = rules
|
|
||||||
|
|
||||||
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
log.Trace().Interface("ACL", rules).Msg("ACL rules generated")
|
||||||
|
h.aclRules = rules
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -83,16 +86,23 @@ func (h *Headscale) LoadACLPolicy(path string) error {
|
||||||
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||||
rules := []tailcfg.FilterRule{}
|
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 {
|
for index, acl := range h.aclPolicy.ACLs {
|
||||||
if acl.Action != "accept" {
|
if acl.Action != "accept" {
|
||||||
return nil, errInvalidAction
|
return nil, errInvalidAction
|
||||||
}
|
}
|
||||||
|
|
||||||
filterRule := tailcfg.FilterRule{}
|
|
||||||
|
|
||||||
srcIPs := []string{}
|
srcIPs := []string{}
|
||||||
for innerIndex, user := range acl.Users {
|
for innerIndex, user := range acl.Users {
|
||||||
srcs, err := h.generateACLPolicySrcIP(user)
|
srcs, err := h.generateACLPolicySrcIP(machines, *h.aclPolicy, user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, User %d", index, innerIndex)
|
||||||
|
@ -101,11 +111,10 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||||
}
|
}
|
||||||
srcIPs = append(srcIPs, srcs...)
|
srcIPs = append(srcIPs, srcs...)
|
||||||
}
|
}
|
||||||
filterRule.SrcIPs = srcIPs
|
|
||||||
|
|
||||||
destPorts := []tailcfg.NetPortRange{}
|
destPorts := []tailcfg.NetPortRange{}
|
||||||
for innerIndex, ports := range acl.Ports {
|
for innerIndex, ports := range acl.Ports {
|
||||||
dests, err := h.generateACLPolicyDestPorts(ports)
|
dests, err := h.generateACLPolicyDestPorts(machines, *h.aclPolicy, ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
Msgf("Error parsing ACL %d, Port %d", index, innerIndex)
|
||||||
|
@ -124,11 +133,17 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||||
return rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) generateACLPolicySrcIP(u string) ([]string, error) {
|
func (h *Headscale) generateACLPolicySrcIP(
|
||||||
return h.expandAlias(u)
|
machines []Machine,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
|
u string,
|
||||||
|
) ([]string, error) {
|
||||||
|
return expandAlias(machines, aclPolicy, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) generateACLPolicyDestPorts(
|
func (h *Headscale) generateACLPolicyDestPorts(
|
||||||
|
machines []Machine,
|
||||||
|
aclPolicy ACLPolicy,
|
||||||
d string,
|
d string,
|
||||||
) ([]tailcfg.NetPortRange, error) {
|
) ([]tailcfg.NetPortRange, error) {
|
||||||
tokens := strings.Split(d, ":")
|
tokens := strings.Split(d, ":")
|
||||||
|
@ -149,11 +164,11 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
||||||
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
expanded, err := h.expandAlias(alias)
|
expanded, err := expandAlias(machines, aclPolicy, alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ports, err := h.expandPorts(tokens[len(tokens)-1])
|
ports, err := expandPorts(tokens[len(tokens)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -172,21 +187,28 @@ func (h *Headscale) generateACLPolicyDestPorts(
|
||||||
return dests, nil
|
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 == "*" {
|
if alias == "*" {
|
||||||
return []string{"*"}, nil
|
return []string{"*"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(alias, "group:") {
|
if strings.HasPrefix(alias, "group:") {
|
||||||
if _, ok := h.aclPolicy.Groups[alias]; !ok {
|
namespaces, err := expandGroup(aclPolicy, alias)
|
||||||
return nil, errInvalidGroup
|
if err != nil {
|
||||||
|
return ips, err
|
||||||
}
|
}
|
||||||
ips := []string{}
|
for _, n := range namespaces {
|
||||||
for _, n := range h.aclPolicy.Groups[alias] {
|
nodes := filterMachinesByNamespace(machines, n)
|
||||||
nodes, err := h.ListMachinesInNamespace(n)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errInvalidNamespace
|
|
||||||
}
|
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
ips = append(ips, node.IPAddresses.ToStringSlice()...)
|
||||||
}
|
}
|
||||||
|
@ -196,35 +218,23 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(alias, "tag:") {
|
if strings.HasPrefix(alias, "tag:") {
|
||||||
if _, ok := h.aclPolicy.TagOwners[alias]; !ok {
|
owners, err := expandTagOwners(aclPolicy, alias)
|
||||||
return nil, errInvalidTag
|
if err != nil {
|
||||||
|
return ips, err
|
||||||
}
|
}
|
||||||
|
for _, namespace := range owners {
|
||||||
// This will have HORRIBLE performance.
|
machines := filterMachinesByNamespace(machines, namespace)
|
||||||
// We need to change the data model to better store tags
|
for _, machine := range machines {
|
||||||
machines := []Machine{}
|
if len(machine.HostInfo) == 0 {
|
||||||
if err := h.db.Where("registered").Find(&machines).Error; err != nil {
|
continue
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ips := []string{}
|
|
||||||
for _, machine := range machines {
|
|
||||||
hostinfo := tailcfg.Hostinfo{}
|
|
||||||
if len(machine.HostInfo) != 0 {
|
|
||||||
hi, err := machine.HostInfo.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(hi, &hostinfo)
|
hi, err := machine.GetHostInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ips, err
|
||||||
}
|
}
|
||||||
|
for _, t := range hi.RequestTags {
|
||||||
// FIXME: Check TagOwners allows this
|
if alias == t {
|
||||||
for _, t := range hostinfo.RequestTags {
|
|
||||||
if alias[4:] == t {
|
|
||||||
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
ips = append(ips, machine.IPAddresses.ToStringSlice()...)
|
||||||
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,38 +243,82 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) {
|
||||||
return ips, nil
|
return ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := h.GetNamespace(alias)
|
// if alias is a namespace
|
||||||
if err == nil {
|
nodes := filterMachinesByNamespace(machines, alias)
|
||||||
nodes, err := h.ListMachinesInNamespace(n.Name)
|
nodes, err := excludeCorrectlyTaggedNodes(aclPolicy, nodes, alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return ips, err
|
||||||
}
|
}
|
||||||
ips := []string{}
|
for _, n := range nodes {
|
||||||
for _, n := range nodes {
|
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
||||||
ips = append(ips, n.IPAddresses.ToStringSlice()...)
|
}
|
||||||
}
|
if len(ips) > 0 {
|
||||||
|
|
||||||
return ips, nil
|
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
|
return []string{h.String()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if alias is an IP
|
||||||
ip, err := netaddr.ParseIP(alias)
|
ip, err := netaddr.ParseIP(alias)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []string{ip.String()}, nil
|
return []string{ip.String()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if alias is an CIDR
|
||||||
cidr, err := netaddr.ParseIPPrefix(alias)
|
cidr, err := netaddr.ParseIPPrefix(alias)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []string{cidr.String()}, 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 == "*" {
|
if portsStr == "*" {
|
||||||
return &[]tailcfg.PortRange{
|
return &[]tailcfg.PortRange{
|
||||||
{First: portRangeBegin, Last: portRangeEnd},
|
{First: portRangeBegin, Last: portRangeEnd},
|
||||||
|
@ -306,3 +360,64 @@ func (h *Headscale) expandPorts(portsStr string) (*[]tailcfg.PortRange, error) {
|
||||||
|
|
||||||
return &ports, nil
|
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
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
|
"gorm.io/datatypes"
|
||||||
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Suite) TestWrongPath(c *check.C) {
|
func (s *Suite) TestWrongPath(c *check.C) {
|
||||||
|
@ -52,6 +59,245 @@ func (s *Suite) TestBasicRule(c *check.C) {
|
||||||
c.Assert(rules, check.NotNil)
|
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) {
|
func (s *Suite) TestPortRange(c *check.C) {
|
||||||
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
|
err := app.LoadACLPolicy("./tests/acls/acl_policy_basic_range.hujson")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
@ -94,7 +340,7 @@ func (s *Suite) TestPortNamespace(c *check.C) {
|
||||||
ips, _ := app.getAvailableIPs()
|
ips, _ := app.getAvailableIPs()
|
||||||
machine := Machine{
|
machine := Machine{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
MachineKey: "foo",
|
MachineKey: "12345",
|
||||||
NodeKey: "bar",
|
NodeKey: "bar",
|
||||||
DiscoKey: "faa",
|
DiscoKey: "faa",
|
||||||
Name: "testmachine",
|
Name: "testmachine",
|
||||||
|
@ -165,3 +411,723 @@ func (s *Suite) TestPortGroup(c *check.C) {
|
||||||
c.Assert(len(ips), check.Equals, 1)
|
c.Assert(len(ips), check.Equals, 1)
|
||||||
c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String())
|
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
|
var respBody []byte
|
||||||
if req.Compress == "zstd" {
|
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)
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
srcCompressed := encoder.EncodeAll(src, nil)
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
|
@ -290,7 +299,16 @@ func (h *Headscale) getMapKeepAliveResponse(
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
var err error
|
var err error
|
||||||
if mapRequest.Compress == "zstd" {
|
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)
|
encoder, _ := zstd.NewWriter(nil)
|
||||||
srcCompressed := encoder.EncodeAll(src, nil)
|
srcCompressed := encoder.EncodeAll(src, nil)
|
||||||
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
respBody = h.privateKey.SealTo(machineKey, srcCompressed)
|
||||||
|
|
20
dns.go
20
dns.go
|
@ -163,7 +163,15 @@ func getMapResponseDNSConfig(
|
||||||
dnsConfig = dnsConfigOrig.Clone()
|
dnsConfig = dnsConfigOrig.Clone()
|
||||||
dnsConfig.Domains = append(
|
dnsConfig.Domains = append(
|
||||||
dnsConfig.Domains,
|
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)
|
namespaceSet := set.New(set.ThreadSafe)
|
||||||
|
@ -171,8 +179,14 @@ func getMapResponseDNSConfig(
|
||||||
for _, p := range peers {
|
for _, p := range peers {
|
||||||
namespaceSet.Add(p.Namespace)
|
namespaceSet.Add(p.Namespace)
|
||||||
}
|
}
|
||||||
for _, namespace := range namespaceSet.List() {
|
for _, ns := range namespaceSet.List() {
|
||||||
dnsRoute := fmt.Sprintf("%s.%s", namespace.(Namespace).Name, baseDomain)
|
namespace, ok := ns.(Namespace)
|
||||||
|
if !ok {
|
||||||
|
dnsConfig = dnsConfigOrig
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dnsRoute := fmt.Sprintf("%v.%v", namespace.Name, baseDomain)
|
||||||
dnsConfig.Routes[dnsRoute] = nil
|
dnsConfig.Routes[dnsRoute] = nil
|
||||||
}
|
}
|
||||||
} else {
|
} 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.
|
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
|
### Apple devices
|
||||||
|
|
||||||
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
An endpoint with information on how to connect your Apple devices (currently macOS only) is available at `/apple` on your running instance.
|
||||||
|
|
141
docs/acls.md
Normal file
141
docs/acls.md
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
# ACLs use case example
|
||||||
|
|
||||||
|
Let's build an example use case for a small business (It may be the place where
|
||||||
|
ACL's are the most useful).
|
||||||
|
|
||||||
|
We have a small company with a boss, an admin, two developers and an intern.
|
||||||
|
|
||||||
|
The boss should have access to all servers but not to the users hosts. Admin
|
||||||
|
should also have access to all hosts except that their permissions should be
|
||||||
|
limited to maintaining the hosts (for example purposes). The developers can do
|
||||||
|
anything they want on dev hosts, but only watch on productions hosts. Intern
|
||||||
|
can only interact with the development servers.
|
||||||
|
|
||||||
|
Each user have at least a device connected to the network and we have some
|
||||||
|
servers.
|
||||||
|
|
||||||
|
- database.prod
|
||||||
|
- database.dev
|
||||||
|
- app-server1.prod
|
||||||
|
- app-server1.dev
|
||||||
|
- billing.internal
|
||||||
|
|
||||||
|
## Setup of the network
|
||||||
|
|
||||||
|
Let's create the namespaces. Each user should have his own namespace. The users
|
||||||
|
here are represented as namespaces.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
headscale namespaces create boss
|
||||||
|
headscale namespaces create admin1
|
||||||
|
headscale namespaces create dev1
|
||||||
|
headscale namespaces create dev2
|
||||||
|
headscale namespaces create intern1
|
||||||
|
```
|
||||||
|
|
||||||
|
We don't need to create namespaces for the servers because the servers will be
|
||||||
|
tagged. When registering the servers we will need to add the flag
|
||||||
|
`--advertised-tags=tag:<tag1>,tag:<tag2>`, and the user (namespace) that is
|
||||||
|
registering the server should be allowed to do it. Since anyone can add tags to
|
||||||
|
a server they can register, the check of the tags is done on headscale server
|
||||||
|
and only valid tags are applied. A tag is valid if the namespace that is
|
||||||
|
registering it is allowed to do it.
|
||||||
|
|
||||||
|
Here are the ACL's to implement the same permissions as above:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// groups are collections of users having a common scope. A user can be in multiple groups
|
||||||
|
// groups cannot be composed of groups
|
||||||
|
"groups": {
|
||||||
|
"group:boss": ["boss"],
|
||||||
|
"group:dev": ["dev1", "dev2"],
|
||||||
|
"group:admin": ["admin1"],
|
||||||
|
"group:intern": ["intern1"]
|
||||||
|
},
|
||||||
|
// tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.
|
||||||
|
// This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag)
|
||||||
|
// and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)
|
||||||
|
"tagOwners": {
|
||||||
|
// the administrators can add servers in production
|
||||||
|
"tag:prod-databases": ["group:admin"],
|
||||||
|
"tag:prod-app-servers": ["group:admin"],
|
||||||
|
|
||||||
|
// the boss can tag any server as internal
|
||||||
|
"tag:internal": ["group:boss"],
|
||||||
|
|
||||||
|
// dev can add servers for dev purposes as well as admins
|
||||||
|
"tag:dev-databases": ["group:admin", "group:dev"],
|
||||||
|
"tag:dev-app-servers": ["group:admin", "group:dev"]
|
||||||
|
|
||||||
|
// interns cannot add servers
|
||||||
|
},
|
||||||
|
"acls": [
|
||||||
|
// boss have access to all servers
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"users": ["group:boss"],
|
||||||
|
"ports": [
|
||||||
|
"tag:prod-databases:*",
|
||||||
|
"tag:prod-app-servers:*",
|
||||||
|
"tag:internal:*",
|
||||||
|
"tag:dev-databases:*",
|
||||||
|
"tag:dev-app-servers:*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// admin have only access to administrative ports of the servers
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"users": ["group:admin"],
|
||||||
|
"ports": [
|
||||||
|
"tag:prod-databases:22",
|
||||||
|
"tag:prod-app-servers:22",
|
||||||
|
"tag:internal:22",
|
||||||
|
"tag:dev-databases:22",
|
||||||
|
"tag:dev-app-servers:22"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// developers have access to databases servers and application servers on all ports
|
||||||
|
// they can only view the applications servers in prod and have no access to databases servers in production
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"users": ["group:dev"],
|
||||||
|
"ports": [
|
||||||
|
"tag:dev-databases:*",
|
||||||
|
"tag:dev-app-servers:*",
|
||||||
|
"tag:prod-app-servers:80,443"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// servers should be able to talk to database. Database should not be able to initiate connections to
|
||||||
|
// applications servers
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"users": ["tag:dev-app-servers"],
|
||||||
|
"ports": ["tag:dev-databases:5432"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"users": ["tag:prod-app-servers"],
|
||||||
|
"ports": ["tag:prod-databases:5432"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// interns have access to dev-app-servers only in reading mode
|
||||||
|
{
|
||||||
|
"action": "accept",
|
||||||
|
"users": ["group:intern"],
|
||||||
|
"ports": ["tag:dev-app-servers:80,443"]
|
||||||
|
},
|
||||||
|
|
||||||
|
// We still have to allow internal namespaces communications since nothing guarantees that each user have
|
||||||
|
// their own namespaces.
|
||||||
|
{ "action": "accept", "users": ["boss"], "ports": ["boss:*"] },
|
||||||
|
{ "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] },
|
||||||
|
{ "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] },
|
||||||
|
{ "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] },
|
||||||
|
{ "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
193
machine.go
193
machine.go
|
@ -119,6 +119,118 @@ func (machine Machine) isExpired() bool {
|
||||||
return time.Now().UTC().After(*machine.Expiry)
|
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) {
|
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
|
@ -206,39 +318,54 @@ func (h *Headscale) getSharedTo(machine *Machine) (Machines, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
||||||
direct, err := h.getDirectPeers(machine)
|
var peers Machines
|
||||||
if err != nil {
|
var err error
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot fetch peers")
|
|
||||||
|
|
||||||
return Machines{}, err
|
// If ACLs rules are defined, filter visible host list with the ACLs
|
||||||
|
// else use the classic namespace scope
|
||||||
|
if h.aclPolicy != nil {
|
||||||
|
var machines []Machine
|
||||||
|
machines, err = h.ListAllMachines()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Error retrieving list of machines")
|
||||||
|
|
||||||
|
return Machines{}, err
|
||||||
|
}
|
||||||
|
peers = getFilteredByACLPeers(machines, h.aclRules, machine)
|
||||||
|
} else {
|
||||||
|
direct, err := h.getDirectPeers(machine)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot fetch peers")
|
||||||
|
|
||||||
|
return Machines{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
shared, err := h.getShared(machine)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot fetch peers")
|
||||||
|
|
||||||
|
return Machines{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sharedTo, err := h.getSharedTo(machine)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().
|
||||||
|
Caller().
|
||||||
|
Err(err).
|
||||||
|
Msg("Cannot fetch peers")
|
||||||
|
|
||||||
|
return Machines{}, err
|
||||||
|
}
|
||||||
|
peers = append(direct, shared...)
|
||||||
|
peers = append(peers, sharedTo...)
|
||||||
}
|
}
|
||||||
|
|
||||||
shared, err := h.getShared(machine)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot fetch peers")
|
|
||||||
|
|
||||||
return Machines{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sharedTo, err := h.getSharedTo(machine)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().
|
|
||||||
Caller().
|
|
||||||
Err(err).
|
|
||||||
Msg("Cannot fetch peers")
|
|
||||||
|
|
||||||
return Machines{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
peers := append(direct, shared...)
|
|
||||||
peers = append(peers, sharedTo...)
|
|
||||||
|
|
||||||
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
sort.Slice(peers, func(i, j int) bool { return peers[i].ID < peers[j].ID })
|
||||||
|
|
||||||
log.Trace().
|
log.Trace().
|
||||||
|
@ -597,7 +724,11 @@ func (machine Machine) toNode(
|
||||||
hostname = fmt.Sprintf(
|
hostname = fmt.Sprintf(
|
||||||
"%s.%s.%s",
|
"%s.%s.%s",
|
||||||
machine.Name,
|
machine.Name,
|
||||||
machine.Namespace.Name,
|
strings.ReplaceAll(
|
||||||
|
machine.Namespace.Name,
|
||||||
|
"@",
|
||||||
|
".",
|
||||||
|
), // Replace @ with . for valid domain for machine
|
||||||
baseDomain,
|
baseDomain,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
262
machine_test.go
262
machine_test.go
|
@ -1,11 +1,15 @@
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Suite) TestGetMachine(c *check.C) {
|
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")
|
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) {
|
func (s *Suite) TestExpireMachine(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("test")
|
namespace, err := app.CreateNamespace("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
@ -208,3 +295,178 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
|
||||||
c.Assert(deserialized[i], check.Equals, input[i])
|
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).
|
Str("machine", machine.Name).
|
||||||
Msg("Found machine in database")
|
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.Name = req.Hostinfo.Hostname
|
||||||
machine.HostInfo = datatypes.JSON(hostinfo)
|
machine.HostInfo = datatypes.JSON(hostinfo)
|
||||||
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
|
machine.DiscoKey = DiscoPublicKeyStripPrefix(req.DiscoKey)
|
||||||
now := time.Now().UTC()
|
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:
|
// From Tailscale client:
|
||||||
//
|
//
|
||||||
// ReadOnly is whether the client just wants to fetch the MapResponse,
|
// 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
|
// The intended use is for clients to discover the DERP map at start-up
|
||||||
// before their first real endpoint update.
|
// before their first real endpoint update.
|
||||||
if !req.ReadOnly {
|
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.Endpoints = datatypes.JSON(endpoints)
|
||||||
machine.LastSeen = &now
|
machine.LastSeen = &now
|
||||||
}
|
}
|
||||||
|
|
10
utils.go
10
utils.go
|
@ -212,6 +212,16 @@ func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) {
|
||||||
return ips, nil
|
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 {
|
func containsIPs(ips []netaddr.IP, ip netaddr.IP) bool {
|
||||||
for _, v := range ips {
|
for _, v := range ips {
|
||||||
if v == ip {
|
if v == ip {
|
||||||
|
|
Loading…
Reference in a new issue